104 lines
5.3 KiB
Markdown
104 lines
5.3 KiB
Markdown
# `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
|