# `service-to-service` — token relay, client credentials, exchange and mTLS Companion project for [**Securing Spring Boot Microservices: Token Relay, Service-to-Service JWT and mTLS**](https://ankurm.com/spring-boot-microservices-token-relay-mtls/) on ankurm.com. Four real processes and a real Spring Authorization Server, so that questions like "whose identity arrives at the last service?" and "what does that service actually check?" have transcripts for answers. Everything under [`docs/output/`](docs/output/) was produced by `scripts/run-all.sh`. ## Versions | | Version | Notes | |---|---|---| | JDK | 25 (Temurin 25.0.4.1+1) | current LTS | | Spring Boot | 4.1.1 | inherited as parent | | Spring Framework | 7.0.9 | | | Spring Security | 7.1.1 | resource server, OAuth2 client, authorization server | | Spring Cloud | 2025.1.3 (gateway 5.0.3) | **built against Boot 4.0.8** — see [docs/01](docs/01-the-four-processes.md) | | Tomcat | 11.0.24 | | Versions were read from `repo1.maven.org/.../maven-metadata.xml`. ## Quickstart ```bash ./scripts/run.sh # all four processes, in order TOKEN=$(./scripts/user-token.sh) # a real authorization_code + PKCE flow, in curl ./scripts/claims.sh "$TOKEN" curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/relay curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/client-credentials curl -H "Authorization: Bearer $TOKEN" 127.0.0.1:8081/edge/exchange STRICT=true ./scripts/run.sh # issuer + audience + RFC 9068 validation ./scripts/certs.sh && ./scripts/run-all.sh mvn test # the 8 validator assertions ./scripts/stop.sh ``` The user is `alice` / `password`. ## Processes | Process | Port | Main class | Role | |---|---|---|---| | authserver | 9000 | [`AuthServerApplication`](src/main/java/com/ankurm/s2s/authserver/AuthServerApplication.java) | Spring Authorization Server. Mints every token used here | | gateway | 8080 | [`GatewayApplication`](src/main/java/com/ankurm/s2s/gateway/GatewayApplication.java) | Spring Cloud Gateway Server MVC, with and without `TokenRelay` | | edge | 8081 | [`EdgeApplication`](src/main/java/com/ankurm/s2s/edge/EdgeApplication.java) | Resource server **and** OAuth2 client | | downstream | 8082 | [`DownstreamApplication`](src/main/java/com/ankurm/s2s/downstream/DownstreamApplication.java) | Resource server. Reports who it thinks is calling | | mtls | 8443 | [`MtlsApplication`](src/main/java/com/ankurm/s2s/mtls/MtlsApplication.java) | Certificate authentication instead of tokens | ## Endpoints | Endpoint | Strategy | |---|---| | `GET /edge/naive` | No token forwarded — the control | | `GET /edge/relay` | The incoming bearer token, unchanged | | `GET /edge/client-credentials` | The edge service's own identity | | `GET /edge/exchange` | RFC 8693 token exchange | | `GET /edge/relay-async` | Relay from another thread — the `ThreadLocal` trap | | `GET /orders` | Downstream. Echoes `sub`, `aud`, `scope`, `client_id`, `cnf` | | `GET /mtls/whoami` | The verified certificate identity | | `GET /mtls/trusted-header` | An identity taken from a header, verified by nothing | ## Switches | Switch | Effect | |---|---| | `STRICT=true ./scripts/run.sh` | Authorization server emits RFC 9068 tokens; downstream validates issuer, audience and the required-claim set | | `RS_LOG_LEVEL` / `CLIENT_LOG_LEVEL` / `AS_LOG_LEVEL` / `GATEWAY_LOG_LEVEL` | `DEBUG` on the corresponding package | ## Documentation | Chapter | | |---|---| | [01](docs/01-the-four-processes.md) | The four processes, a browser flow in curl, and four things that cost time | | [02](docs/02-three-ways-to-get-a-token.md) | Relay, client credentials, token exchange — and the thread that loses the token | | [03](docs/03-restclient-interceptors.md) | `OAuth2ClientHttpRequestInterceptor` and which `OAuth2AuthorizedClientManager` | | [04](docs/04-what-is-not-validated.md) | **What a resource server does not validate by default** | | [05](docs/05-the-gateway.md) | What `TokenRelay` actually relays | | [06](docs/06-mtls.md) | Mesh mTLS versus in-application mTLS | | [07](docs/07-choosing.md) | Choosing, and whether you need any of it | ## Captured output | File | | |---|---| | [01-user-token.txt](docs/output/01-user-token.txt) | A complete authorization_code + PKCE flow, in curl | | [02-five-strategies.txt](docs/output/02-five-strategies.txt) | Five propagation strategies, one request | | [03-audience-ignored.txt](docs/output/03-audience-ignored.txt) | A token for another service, accepted with HTTP 200 | | [04-gateway-token-relay.txt](docs/output/04-gateway-token-relay.txt) | The same route with and without `TokenRelay` | | [05-strict-validation.txt](docs/output/05-strict-validation.txt) | Strict validation, and what turning it on breaks | | [06-mtls.txt](docs/output/06-mtls.txt) | Two certificates with the same subject and different issuers | | [07-tests.txt](docs/output/07-tests.txt) | `mvn test` | ## Related modules - [`filter-chain/`](../filter-chain/README.md) — where `BearerTokenAuthenticationFilter` sits - [`context-propagation/`](../context-propagation/README.md) — why `/edge/relay-async` returns 401 - [`cors-csrf/`](../cors-csrf/README.md) — the browser-facing half of the same problem - [`method-security/`](../method-security/README.md) — turning a scope into an authorization decision