[← 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(scopes.convert(jwt)); // keep these List 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)