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.
97 lines
3.9 KiB
Markdown
97 lines
3.9 KiB
Markdown
[← 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)
|