Add Spring Authorization Server project: OAuth2/OIDC provider, client and resource server
Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.
Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
in 7.0, and both configuration classes moved into spring-security-config
- ClientSettings.requireProofKey flipped from false to true, on the authorization server
(1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
- requireProofKey(false) does not make PKCE optional for a public client; the code
verifier is that client's only authentication at the token endpoint
- MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called
Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
This commit is contained in:
96
docs/authorization-server/06-resource-server.md
Normal file
96
docs/authorization-server/06-resource-server.md
Normal file
@@ -0,0 +1,96 @@
|
||||
[← 05 Token customisation](05-token-customisation.md) · [index](README.md) · next: [07 — Diagnostics](07-diagnostics.md)
|
||||
|
||||
# The resource server side
|
||||
|
||||
Source:
|
||||
[`SecurityConfig.java`](../../authorization-server/resource-server/src/main/java/com/ankurm/rs/SecurityConfig.java).
|
||||
The deeper treatment of this half lives in [`docs/12`–`18`](../12-issuer-and-audience.md);
|
||||
this chapter is only what changes when the issuer is *yours*.
|
||||
|
||||
## One property, and what it buys
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: http://localhost:9000
|
||||
```
|
||||
|
||||
At startup, Spring fetches `/.well-known/openid-configuration`, reads `jwks_uri` from it,
|
||||
and builds a decoder. You get signature verification, `exp`/`nbf`, and an `iss` check.
|
||||
|
||||
You do **not** get an audience check. See [05](05-token-customisation.md) for why that
|
||||
matters when the default `aud` is the client id.
|
||||
|
||||
## The startup coupling nobody mentions
|
||||
|
||||
If the authorization server is not reachable, the resource server does not start
|
||||
([`as-rs-startup-failure.txt`](../output/as-rs-startup-failure.txt)):
|
||||
|
||||
```
|
||||
java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided
|
||||
Issuer of "http://localhost:9000"
|
||||
org.springframework.web.client.ResourceAccessException: I/O error on GET request for
|
||||
"http://localhost:9000/.well-known/openid-configuration": Connection refused
|
||||
```
|
||||
|
||||
This is deliberate — fail fast rather than serve unauthenticated traffic — but
|
||||
it means a provider outage during a rolling deploy takes every API with it. If that is not
|
||||
acceptable, configure `jwk-set-uri` directly and validate `iss` yourself, which removes the
|
||||
discovery call at the cost of pinning the endpoint.
|
||||
|
||||
## The issuer string must match exactly
|
||||
|
||||
`http://localhost:9000` and `http://localhost:9000/` are different values. A mismatch fails
|
||||
at *validation* time with `The iss claim is not valid`, not at startup, so it looks like a
|
||||
token problem rather than a configuration one.
|
||||
|
||||
## Mapping custom claims without deleting the scopes
|
||||
|
||||
The provider writes a `roles` claim. Mapping it is easy to get wrong in one specific way:
|
||||
|
||||
```java
|
||||
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
|
||||
var authorities = new ArrayList<GrantedAuthority>(scopes.convert(jwt)); // keep these
|
||||
List<String> roles = jwt.getClaimAsStringList("roles");
|
||||
if (roles != null) {
|
||||
roles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_" + r)));
|
||||
}
|
||||
return authorities;
|
||||
});
|
||||
```
|
||||
|
||||
Returning a converter that only handles `roles` silently deletes every `SCOPE_*` authority,
|
||||
which turns `hasAuthority("SCOPE_orders.read")` into a 403 on a perfectly valid token. The
|
||||
same trap, in its properties-driven form, is
|
||||
[`docs/14-authentication-converter.md`](../14-authentication-converter.md).
|
||||
|
||||
## What the authorities actually look like
|
||||
|
||||
From a real request ([`as-authcode-pkce.txt`](../output/as-authcode-pkce.txt)):
|
||||
|
||||
```json
|
||||
["SCOPE_openid","ROLE_USER","SCOPE_orders.read","ROLE_ADMIN",
|
||||
"FactorGrantedAuthority [authority=FACTOR_BEARER, issuedAt=2026-08-24T01:52:21.799Z]"]
|
||||
```
|
||||
|
||||
`FactorGrantedAuthority` is new in Spring Security 7 — it records *how* the principal
|
||||
authenticated, for multi-factor authorisation rules. It shows up in every authority list now.
|
||||
Code that asserts on the exact contents of `getAuthorities()` will fail on upgrade.
|
||||
|
||||
## Protected resource metadata, also new
|
||||
|
||||
The `WWW-Authenticate` header now carries a `resource_metadata` parameter:
|
||||
|
||||
```
|
||||
WWW-Authenticate: Bearer error="invalid_token", error_description="…",
|
||||
resource_metadata="http://localhost:8090/.well-known/oauth-protected-resource"
|
||||
```
|
||||
|
||||
That is [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728), emitted by default. It
|
||||
tells a client where to learn what this API expects. Harmless, but it is a new endpoint on
|
||||
your resource server that you did not add.
|
||||
|
||||
Next: [07 — Diagnostics](07-diagnostics.md)
|
||||
Reference in New Issue
Block a user