-------------------------------------------------------------------------- resource server profiles: stub,roles,audience,attyp -------------------------------------------------------------------------- -------------------------------------------------------------------------- 1. A correct token -------------------------------------------------------------------------- header: {"kid": "stub-key-1", "typ": "JWT", "alg": "RS256"} iss "http://localhost:9000" aud "reports-api" scope "profile:read reports:read" preferred_username "alice" realm_access {"roles": ["USER"]} resource_access {"reports-api": {"roles": ["reports-reader"]}} $ GET /api/me HTTP 200 { "name": "alice", "authorities": [ "FACTOR_BEARER", "ROLE_USER", "ROLE_reports-reader", "SCOPE_profile:read", "SCOPE_reports:read" ], "iss": "http://localhost:9000", "aud": [ "reports-api" ], "typ": "JWT", "kid": "stub-key-1", "exp": "2026-08-23T10:32:56Z" } -------------------------------------------------------------------------- 2. Signed by the right key, but iss says something else -------------------------------------------------------------------------- The signature verifies. The key is the same key. Only the string differs. header: {"kid": "stub-key-1", "typ": "JWT", "alg": "RS256"} iss "http://localhost:9000/other" aud "reports-api" scope "profile:read reports:read" preferred_username "alice" realm_access {"roles": ["USER"]} resource_access {"reports-api": {"roles": ["reports-reader"]}} $ GET /api/me HTTP 401 WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: The iss claim is not valid", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8081/.well-known/oauth-protected-resource" -------------------------------------------------------------------------- 3. A token minted for a different service in the same realm -------------------------------------------------------------------------- This is the one that silently works when nothing checks aud. header: {"kid": "stub-key-1", "typ": "JWT", "alg": "RS256"} iss "http://localhost:9000" aud "billing-api" scope "profile:read reports:read" preferred_username "alice" realm_access {"roles": ["USER"]} resource_access {"reports-api": {"roles": ["reports-reader"]}} $ GET /api/me HTTP 401 WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: The aud claim is not valid", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8081/.well-known/oauth-protected-resource" -------------------------------------------------------------------------- 4. Expired 90 seconds ago -------------------------------------------------------------------------- The default clock skew is 60s, so a token has to be more than a minute stale before JwtTimestampValidator refuses it. $ GET /api/me HTTP 401 WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Jwt expired at 2026-08-23T10:26:26Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8081/.well-known/oauth-protected-resource" -------------------------------------------------------------------------- 5. Expired 30 seconds ago - inside the default clock skew -------------------------------------------------------------------------- $ GET /api/me HTTP 200 { "name": "alice", "authorities": [ "FACTOR_BEARER", "ROLE_USER", "ROLE_reports-reader", "SCOPE_profile:read", "SCOPE_reports:read" ], "iss": "http://localhost:9000", "aud": [ "reports-api" ], "typ": "JWT", "kid": "stub-key-1", "exp": "2026-08-23T10:27:26Z" } -------------------------------------------------------------------------- 6. typ=at+jwt, which RFC 9068 says an access token SHOULD carry -------------------------------------------------------------------------- The default validator stack contains JwtTypeValidator.jwt(), which accepts only an absent typ or typ=JWT. Whether this passes depends on the attyp profile. header: {"kid": "stub-key-1", "typ": "at+jwt", "alg": "RS256"} iss "http://localhost:9000" aud "reports-api" scope "profile:read reports:read" preferred_username "alice" realm_access {"roles": ["USER"]} resource_access {"reports-api": {"roles": ["reports-reader"]}} $ GET /api/me HTTP 200 { "name": "alice", "authorities": [ "FACTOR_BEARER", "ROLE_USER", "ROLE_reports-reader", "SCOPE_profile:read", "SCOPE_reports:read" ], "iss": "http://localhost:9000", "aud": [ "reports-api" ], "typ": "at+jwt", "kid": "stub-key-1", "exp": "2026-08-23T10:32:56Z" } -------------------------------------------------------------------------- 7. No token at all -------------------------------------------------------------------------- $ GET /api/me HTTP 401 WWW-Authenticate: Bearer resource_metadata="http://localhost:8081/.well-known/oauth-protected-resource" $ GET /api/public/ping HTTP 200 { "status": "up" } -------------------------------------------------------------------------- 8. The endpoint nobody configured -------------------------------------------------------------------------- Spring Security 7 publishes RFC 9728 protected resource metadata and points the WWW-Authenticate challenge at it. It answers without a token. $ GET /.well-known/oauth-protected-resource HTTP 200 { "resource": "http://localhost:8081", "bearer_methods_supported": [ "header" ], "tls_client_certificate_bound_access_tokens": true }