1
0
Files
spring-auth-demo/docs/authorization-server/06-resource-server.md
Ankur Mhatre 38c0a5f358 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.
2026-08-24 08:12:36 +05:30

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 &mdash; 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 &mdash; Diagnostics](07-diagnostics.md)