1
0

Add the service-to-service module

This commit is contained in:
2026-08-28 10:02:47 +05:30
parent cad813e1ae
commit 0fceb2cd4e
37 changed files with 2566 additions and 4 deletions

View File

@@ -0,0 +1,26 @@
==============================================================================
docs/output/01-user-token.txt
A complete authorization_code + PKCE flow, driven by curl. No browser, no OIDC library.
scripts/user-token.sh, then scripts/claims.sh
==============================================================================
{
"alg": "RS256",
"kid": "<uuid>"
}
{
"aud": "downstream-api",
"exp": <epoch>,
"iat": <epoch>,
"iss": "http://127.0.0.1:9000",
"jti": "<uuid>",
"nbf": <epoch>,
"scope": [
"orders.write",
"orders.read"
],
"sub": "alice"
}
# sub is the human. scope is what the human consented to. aud names the service the
# token was minted for - chapter 4 is about whether anybody looks at it.

View File

@@ -0,0 +1,110 @@
==============================================================================
docs/output/02-five-strategies.txt
The same request into the edge service, five ways of getting a token for the hop to
downstream. Read the sub and scope of each downstream response.
GET /edge/{naive,relay,client-credentials,exchange,relay-async}
==============================================================================
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/naive
{
"strategy": "no token forwarded",
"error": "Unauthorized: 401 Unauthorized: [no body]"
}
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/relay
{
"strategy": "bearer token relayed from the incoming request",
"downstream": {
"service": "downstream:8082",
"strictValidation": false,
"sub": "alice",
"aud": [
"downstream-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.write, orders.read]",
"client_id": null,
"authorities": [
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]",
"SCOPE_orders.read",
"SCOPE_orders.write"
],
"cnf": null,
"orders": [
{
"total": "42.00",
"id": 1
}
]
}
}
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/client-credentials
{
"strategy": "the edge service's own client_credentials token",
"downstream": {
"service": "downstream:8082",
"strictValidation": false,
"sub": "edge-service",
"aud": [
"downstream-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.read]",
"client_id": null,
"authorities": [
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]",
"SCOPE_orders.read"
],
"cnf": null,
"orders": [
{
"total": "42.00",
"id": 1
}
]
}
}
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/exchange
{
"strategy": "RFC 8693 token exchange",
"downstream": {
"service": "downstream:8082",
"strictValidation": false,
"sub": "alice",
"aud": [
"downstream-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.read]",
"client_id": null,
"authorities": [
"SCOPE_orders.read",
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]"
],
"cnf": null,
"orders": [
{
"total": "42.00",
"id": 1
}
]
}
}
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/relay-async
{
"strategy": "relay attempted from a separate thread",
"error": "Unauthorized: 401 Unauthorized: [no body]"
}
# naive - 401. The control.
# relay - sub: alice, scope: [orders.write, orders.read]. The user's own
# token, unchanged, including scopes downstream did not need.
# client-creds - sub: edge-service, scope: [orders.read]. Correctly scoped, and
# the user has disappeared from downstream's audit log.
# exchange - sub: alice, scope: [orders.read]. Both. This is what RFC 8693 is
# for and it is the one nobody reaches for.
# relay-async - 401. The relay interceptor reads SecurityContextHolder, which is
# a ThreadLocal, and the call was made on a different thread.

View File

@@ -0,0 +1,53 @@
==============================================================================
docs/output/03-audience-ignored.txt
A token minted for a DIFFERENT service, presented to the downstream service.
Default validators.
==============================================================================
# the token reporting-service was issued:
{
"alg": "RS256",
"kid": "<uuid>"
}
{
"aud": "reporting-api",
"exp": <epoch>,
"iat": <epoch>,
"iss": "http://127.0.0.1:9000",
"jti": "<uuid>",
"nbf": <epoch>,
"scope": [
"orders.read"
],
"sub": "reporting-service"
}
$ curl -H 'Authorization: Bearer <reporting-service token>' 127.0.0.1:8082/orders
{
"service": "downstream:8082",
"strictValidation": false,
"sub": "reporting-service",
"aud": [
"reporting-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.read]",
"client_id": null,
"authorities": [
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]",
"SCOPE_orders.read"
],
"cnf": null,
"orders": [
{
"total": "42.00",
"id": 1
}
]
}
# HTTP 200. The aud claim says reporting-api. The service is downstream-api.
# JwtValidators.createDefault() is a DelegatingOAuth2TokenValidator over three
# validators - JwtTypeValidator, JwtTimestampValidator and
# X509CertificateThumbprintValidator. Structure, expiry, and certificate binding.
# No issuer. No audience. Read back by reflection in ValidatorContractTests.

View File

@@ -0,0 +1,80 @@
==============================================================================
docs/output/04-gateway-token-relay.txt
Spring Cloud Gateway Server MVC with 'filters: - TokenRelay='.
GET /edge/relay through the gateway on 8080.
==============================================================================
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8080/edge/relay
HTTP/1.1 200
cache-control: no-cache, no-store, max-age=0, must-revalidate
expires: 0
pragma: no-cache
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 0
Content-Type: application/json
{
"strategy": "bearer token relayed from the incoming request",
"downstream": {
"service": "downstream:8082",
"strictValidation": false,
"sub": "alice",
"aud": [
"downstream-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.write, orders.read]",
"client_id": null,
"authorities": [
"SCOPE_orders.read",
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]",
"SCOPE_orders.write"
],
"cnf": null,
"orders": [
{
"total": "42.00",
"id": 1
}
]
}
}
$ curl 127.0.0.1:8080/edge/relay # no Authorization header at all
status 401
# and the identical route with the TokenRelay filter REMOVED:
$ curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8080/norelay/x
{
"strategy": "bearer token relayed from the incoming request",
"downstream": {
"service": "downstream:8082",
"strictValidation": false,
"sub": "alice",
"aud": [
"downstream-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.write, orders.read]",
"client_id": null,
"authorities": [
"SCOPE_orders.read",
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]",
"SCOPE_orders.write"
],
"cnf": null,
"orders": [
{
"total": "42.00",
"id": 1
}
]
}
}
# TokenRelay relays the access token of the currently authenticated USER - the one
# obtained by oauth2Login(). This gateway has no oauth2Login, so there is no
# authorized client to read a token from, and the filter contributes nothing. What
# reaches the edge service is whatever Authorization header the caller sent, because
# the gateway proxied it. TokenRelay is not 'forward the incoming bearer token'.

View File

@@ -0,0 +1,43 @@
==============================================================================
docs/output/05-strict-validation.txt
The same tokens against JwtValidators.createAtJwtValidator().issuer(..).audience(..),
with the authorization server emitting RFC 9068 tokens (typ: at+jwt, client_id claim).
STRICT=true ./scripts/run.sh
==============================================================================
# the wrong-audience token that was accepted in 03:
HTTP/1.1 401
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: The aud claim is not valid", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://127.0.0.1:8082/.well-known/oauth-protected-resource"
# a token minted for this service:
{
"service": "downstream:8082",
"strictValidation": true,
"sub": "edge-service",
"aud": [
"downstream-api"
],
"iss": "http://127.0.0.1:9000",
"scope": "[orders.read]",
"client_id": "edge-service",
"authorities": [
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=<timestamp>]",
"SCOPE_orders.read"
],
"cnf": null,
"orders": [
{
"id": 1,
"total": "42.00"
}
]
}
# and the edge service, which was NOT updated - it still uses Boot's
# auto-configured decoder:
HTTP/1.1 401
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: the given typ value needs to be one of [JWT]", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://127.0.0.1:8081/.well-known/oauth-protected-resource"
# Turning on RFC 9068 at the authorization server broke every resource server that
# still has NimbusJwtDecoder's default JOSE type verifier, and the error message
# mentions neither RFC 9068 nor the authorization server.

View File

@@ -0,0 +1,43 @@
==============================================================================
docs/output/06-mtls.txt
Client-certificate authentication on port 8443, server.ssl.client-auth=need.
Certificates from scripts/certs.sh. edge.crt and rogue.crt have IDENTICAL subjects and
different issuers.
==============================================================================
$ openssl x509 -in target/certs/edge.crt -noout -subject -issuer
subject=CN = edge-service, OU = payments
issuer=CN = Internal Mesh CA
$ openssl x509 -in target/certs/rogue.crt -noout -subject -issuer
subject=CN = edge-service, OU = payments
issuer=CN = Some Other CA
$ curl --cert edge.crt --key edge.key https://localhost:8443/mtls/whoami
{
"principal": "edge-service",
"authenticationType": "PreAuthenticatedAuthenticationToken",
"authorities": [
"ROLE_SERVICE",
"FACTOR_X509"
],
"certificateSubject": "OU=payments,CN=edge-service",
"certificateIssuer": "CN=Internal Mesh CA"
}
$ curl --cert rogue.crt --key rogue.key https://localhost:8443/mtls/whoami
[curl exit 56, http 000]
$ curl https://localhost:8443/mtls/trusted-header # a permitAll() endpoint
[curl exit 56, http 000]
$ curl --cert edge.crt --key edge.key -H 'X-Client-Cert-Subject: CN=payments-service' \
https://localhost:8443/mtls/trusted-header
{"caller":"CN=payments-service","verifiedBy":"nothing. This endpoint believes a header."}
# Three things worth reading twice.
# 1. The rogue certificate fails with curl exit 56 and NO http status. The handshake
# is rejected; the application never sees a request and logs nothing at INFO.
# 2. So does the permitAll() endpoint. client-auth=need is a property of the
# CONNECTOR, not of a path. You cannot expose a public endpoint on that port.
# 3. The last call is what a mesh deployment usually looks like from inside the
# application: an identity taken from a header, verified by nothing.

View File

@@ -0,0 +1,8 @@
==============================================================================
docs/output/07-tests.txt
mvn -B test
==============================================================================
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.225 s -- in com.ankurm.s2s.ValidatorContractTests
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS