Spring Security 7.1 JWT authentication on Spring Boot 4.1
Runnable companion for https://ankurm.com/spring-security-7-1-jwt-authentication-guide/ - login -> token issue -> OncePerRequestFilter -> SecurityContext, end to end - HS256 and RS256 variants (RS256 publishes a real JWKS endpoint) - the same API secured by the built-in oauth2ResourceServer().jwt(), for comparison - 11 documentation chapters under docs/, interlinked with the code - docs/output/ is real captured output, regenerated by scripts/run-all.sh - 13 passing tests pinning the 401-vs-403 contract and the CSRF failure Verified against Spring Boot 4.1.1, Spring Security 7.1.1, JDK 25.0.4.1.
This commit is contained in:
19
docs/output/csrf-trace.txt
Normal file
19
docs/output/csrf-trace.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
==========================================================================
|
||||
Spring Security TRACE log: why a permitAll() endpoint answers 403
|
||||
profiles: hs256,csrfon,trace request: POST /api/auth/login
|
||||
==========================================================================
|
||||
|
||||
DEBUG [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Securing POST /api/auth/login
|
||||
TRACE [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/12)
|
||||
TRACE [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/12)
|
||||
TRACE [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/12)
|
||||
TRACE [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/12)
|
||||
TRACE [nio-8080-exec-2] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/12)
|
||||
TRACE [nio-8080-exec-2] s.s.w.c.CsrfTokenRequestAttributeHandler : Wrote a CSRF token to the following request attributes: [_csrf, org.springframework.security.web.csrf.CsrfToken]
|
||||
TRACE [nio-8080-exec-2] o.s.s.web.csrf.CsrfTokenRequestHandler : Did not find a CSRF token in the [X-XSRF-TOKEN] request header
|
||||
TRACE [nio-8080-exec-2] o.s.s.web.csrf.CsrfTokenRequestHandler : Did not find a CSRF token in the [_csrf] request parameter
|
||||
DEBUG [nio-8080-exec-2] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/auth/login
|
||||
TRACE [nio-8080-exec-2] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
|
||||
|
||||
The chain stops at filter 5 of 12. AuthorizationFilter - the only filter that
|
||||
has ever heard of permitAll() - is number 12. It is never invoked.
|
||||
54
docs/output/csrf-vs-permitall.txt
Normal file
54
docs/output/csrf-vs-permitall.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - CSRF vs permitAll()
|
||||
app started with: --spring.profiles.active=hs256,csrfon
|
||||
==========================================================================
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# A. The login endpoint is permitAll(). POST it anyway.
|
||||
# 403 - and nothing in the authorization rules explains why.
|
||||
|
||||
HTTP 403
|
||||
WWW-Authenticate: Bearer
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# B. GET on the same permitAll() path family works. Only unsafe methods break.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"authenticationRequired": false,
|
||||
"status": "up"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# C. The filter chain, with CsrfFilter present. Count the positions:
|
||||
# CsrfFilter is 5th, AuthorizationFilter is last. The request never
|
||||
# reaches the filter that knows about permitAll().
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
{
|
||||
"matchesThisRequest": true,
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"CsrfFilter",
|
||||
"JwtAuthenticationFilter",
|
||||
"RequestCacheAwareFilter",
|
||||
"SecurityContextHolderAwareRequestFilter",
|
||||
"AnonymousAuthenticationFilter",
|
||||
"SessionManagementFilter",
|
||||
"ExceptionTranslationFilter",
|
||||
"AuthorizationFilter"
|
||||
],
|
||||
"chain": "DefaultSecurityFilterChain defined as 'apiFilterChain' in [class path resource [com/ankurm/jwtauth/config/SecurityConfig.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Csrf, JwtAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, SessionManagement, ExceptionTranslation, Authorization]"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# end
|
||||
253
docs/output/curl-transcript-hs256.txt
Normal file
253
docs/output/curl-transcript-hs256.txt
Normal file
@@ -0,0 +1,253 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - curl transcript
|
||||
target : http://localhost:8080
|
||||
==========================================================================
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 1. Public endpoint, no token. permitAll() means the filter chain lets it through.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"status": "up",
|
||||
"authenticationRequired": false
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 2. Protected endpoint, no token. 401 - we do not know who you are.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 3. Wrong password. Still 401, and the body says nothing about which half was wrong.
|
||||
|
||||
HTTP 401
|
||||
Content-Type: application/problem+json
|
||||
{
|
||||
"detail": "Invalid username or password",
|
||||
"instance": "/api/auth/login",
|
||||
"status": 401,
|
||||
"title": "Authentication failed",
|
||||
"type": "https://ankurm.com/problems/invalid-credentials"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 4. Locked account. 401 with the identical body - no account-state oracle.
|
||||
|
||||
HTTP 401
|
||||
Content-Type: application/problem+json
|
||||
{
|
||||
"detail": "Invalid username or password",
|
||||
"instance": "/api/auth/login",
|
||||
"status": 401,
|
||||
"title": "Authentication failed",
|
||||
"type": "https://ankurm.com/problems/invalid-credentials"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 5. Login as alice (ROLE_USER, SCOPE_profile:read).
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"accessToken": "eyJraWQiOiIxMDNZZDJyWjlMQkh2cUEwSTA5aWY5RWVBMXd1Nm5iejBrVlBRWTR4M1hvIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhbGljZSIsImF1ZCI6Imp3dC1hdXRoLWRlbW8tYXBpIiwibmJmIjoxNzg3Mzc5MzgxLCJzY29wZSI6InByb2ZpbGU6cmVhZCIsInJvbGVzIjpbIlVTRVIiXSwiaXNzIjoiaHR0cHM6Ly9qd3QtYXV0aC1kZW1vLmFua3VybS5jb20iLCJleHAiOjE3ODczODAyODEsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJpYXQiOjE3ODczNzkzODEsImp0aSI6IjkzMGNiNWQ3LWZiNzctNDIxZC05MmViLWU3NzYxMWZiODQxOCJ9.vIbXeXd7_VKM7qYmoUTaYwePWX1x0yGbeuzPmrCvC00",
|
||||
"refreshToken": "eyJraWQiOiIxMDNZZDJyWjlMQkh2cUEwSTA5aWY5RWVBMXd1Nm5iejBrVlBRWTR4M1hvIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhbGljZSIsImF1ZCI6Imp3dC1hdXRoLWRlbW8tYXBpIiwibmJmIjoxNzg3Mzc5MzgxLCJpc3MiOiJodHRwczovL2p3dC1hdXRoLWRlbW8uYW5rdXJtLmNvbSIsImV4cCI6MTc4NzQwODE4MSwidG9rZW5fdHlwZSI6InJlZnJlc2giLCJpYXQiOjE3ODczNzkzODEsImp0aSI6ImM5ODQzZGYyLWM2YTMtNDA4Ni05NDlmLTg0OTBkYTE1ZjI3NCJ9.om1nFZhD-5psTyKXLtW88gfaKdqmDDy5_n3PvBn2spI",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 900
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 6. What is actually inside that token (base64url decode - no signature check).
|
||||
|
||||
--- JOSE header ---
|
||||
{
|
||||
"kid": "103Yd2rZ9LBHvqA0I09if9EeA1wu6nbz0kVPQY4x3Xo",
|
||||
"typ": "JWT",
|
||||
"alg": "HS256"
|
||||
}
|
||||
--- claims ---
|
||||
{
|
||||
"sub": "alice",
|
||||
"aud": "jwt-auth-demo-api",
|
||||
"nbf": 1787379381,
|
||||
"scope": "profile:read",
|
||||
"roles": [
|
||||
"USER"
|
||||
],
|
||||
"iss": "https://jwt-auth-demo.ankurm.com",
|
||||
"exp": 1787380281,
|
||||
"token_type": "access",
|
||||
"iat": 1787379381,
|
||||
"jti": "930cb5d7-fb77-421d-92eb-e77611fb8418"
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 7. The same protected endpoint, now with the token. 200.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"name": "alice",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER",
|
||||
"ROLE_USER",
|
||||
"SCOPE_profile:read"
|
||||
],
|
||||
"authenticationType": "JwtAuthenticationToken",
|
||||
"jti": "930cb5d7-fb77-421d-92eb-e77611fb8418",
|
||||
"issuer": "https://jwt-auth-demo.ankurm.com",
|
||||
"audience": [
|
||||
"jwt-auth-demo-api"
|
||||
],
|
||||
"issuedAt": "2026-08-22T06:16:21Z",
|
||||
"expiresAt": "2026-08-22T06:31:21Z",
|
||||
"algorithm": "HS256",
|
||||
"keyId": "103Yd2rZ9LBHvqA0I09if9EeA1wu6nbz0kVPQY4x3Xo"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 8. alice hits an ADMIN endpoint. 403, not 401 - we know who she is, she may not.
|
||||
|
||||
HTTP 403
|
||||
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 9. Same request with a scope-based rule (@PreAuthorize). Also 403.
|
||||
|
||||
HTTP 403
|
||||
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 10. Login as root and repeat. 200.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"accessToken": "eyJraWQiOiIxMDNZZDJyWjlMQkh2cUEwSTA5aWY5RWVBMXd1Nm5iejBrVlBRWTR4M1hvIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJyb290IiwiYXVkIjoiand0LWF1dGgtZGVtby1hcGkiLCJuYmYiOjE3ODczNzkzODIsInNjb3BlIjoiYWRtaW46cmVhZCBwcm9maWxlOnJlYWQiLCJyb2xlcyI6WyJBRE1JTiIsIlVTRVIiXSwiaXNzIjoiaHR0cHM6Ly9qd3QtYXV0aC1kZW1vLmFua3VybS5jb20iLCJleHAiOjE3ODczODAyODIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJpYXQiOjE3ODczNzkzODIsImp0aSI6ImI0NzRlOGU4LWI1NzUtNDBlOS04MDMyLTEwZjk3ODMwNWE3NiJ9.8bE2iAn5XecooZMSD8AGHl3PqTzw_KAOUugTi-_VgqA",
|
||||
"refreshToken": "eyJraWQiOiIxMDNZZDJyWjlMQkh2cUEwSTA5aWY5RWVBMXd1Nm5iejBrVlBRWTR4M1hvIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJyb290IiwiYXVkIjoiand0LWF1dGgtZGVtby1hcGkiLCJuYmYiOjE3ODczNzkzODIsImlzcyI6Imh0dHBzOi8vand0LWF1dGgtZGVtby5hbmt1cm0uY29tIiwiZXhwIjoxNzg3NDA4MTgyLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImlhdCI6MTc4NzM3OTM4MiwianRpIjoiOWM3NTRjMmEtMDU0Zi00N2ZmLTkzZjktNTAxNTY3ZDI5NjFiIn0.FSR9SUvWqBZZSQMcKJwnUSgF9-B57wmptF-msvpgR8M",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 900
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"requiredRole": "ROLE_ADMIN",
|
||||
"activeUsers": 3
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 11. Tampered payload, original signature. 401 invalid_token.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 12. Garbage where a token should be. 401, and note it is NOT 400.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Malformed token", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 13. Authorization header with no Bearer scheme. The resolver sees no token at all,\n# so this is an authorization failure, not a token failure - note the bare realm.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 14. A refresh token presented as an access token. 401 - the token_type claim.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", error="invalid_token", error_description="This endpoint accepts access tokens only", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 15. Refresh with rotation. New access token, new refresh token.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"accessToken": "eyJraWQiOiIxMDNZZDJyWjlMQkh2cUEwSTA5aWY5RWVBMXd1Nm5iejBrVlBRWTR4M1hvIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhbGljZSIsImF1ZCI6Imp3dC1hdXRoLWRlbW8tYXBpIiwibmJmIjoxNzg3Mzc5MzgyLCJzY29wZSI6InByb2ZpbGU6cmVhZCIsInJvbGVzIjpbIlVTRVIiXSwiaXNzIjoiaHR0cHM6Ly9qd3QtYXV0aC1kZW1vLmFua3VybS5jb20iLCJleHAiOjE3ODczODAyODIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJpYXQiOjE3ODczNzkzODIsImp0aSI6IjkyZjU3ZWMzLTk5OGYtNDkzNC05NjgxLTVkMmM2MWM3OThhOCJ9.pw4kOzw-ZcpcP9VyjgvKG6s5TtSTMrBwU255vG3BJ6g",
|
||||
"refreshToken": "eyJraWQiOiIxMDNZZDJyWjlMQkh2cUEwSTA5aWY5RWVBMXd1Nm5iejBrVlBRWTR4M1hvIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhbGljZSIsImF1ZCI6Imp3dC1hdXRoLWRlbW8tYXBpIiwibmJmIjoxNzg3Mzc5MzgyLCJpc3MiOiJodHRwczovL2p3dC1hdXRoLWRlbW8uYW5rdXJtLmNvbSIsImV4cCI6MTc4NzQwODE4MiwidG9rZW5fdHlwZSI6InJlZnJlc2giLCJpYXQiOjE3ODczNzkzODIsImp0aSI6IjBmNWRjOGVkLWQ0MjItNDUwMS04OTJiLTRlY2Q2MWMxNDA3NyJ9.K4gemuQ8g3DJUmgb61sCWlTKsg3ImSljet4ogd6PJaM",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 900
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 16. Replay the spent refresh token. 401 - rotation makes replay detectable.
|
||||
|
||||
HTTP 401
|
||||
Content-Type: application/problem+json
|
||||
{
|
||||
"detail": "Invalid username or password",
|
||||
"instance": "/api/auth/refresh",
|
||||
"status": 401,
|
||||
"title": "Authentication failed",
|
||||
"type": "https://ankurm.com/problems/invalid-credentials"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 17. Logout revokes the presented access token by its jti.
|
||||
|
||||
HTTP 204
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"revokedTokens": 2
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 18. The revoked token, still cryptographically valid, is now refused.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", error="invalid_token", error_description="Token has been revoked", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 19. The real filter order, read from FilterChainProxy at runtime.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
{
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"JwtAuthenticationFilter",
|
||||
"RequestCacheAwareFilter",
|
||||
"SecurityContextHolderAwareRequestFilter",
|
||||
"AnonymousAuthenticationFilter",
|
||||
"SessionManagementFilter",
|
||||
"ExceptionTranslationFilter",
|
||||
"AuthorizationFilter"
|
||||
],
|
||||
"matchesThisRequest": true,
|
||||
"chain": "DefaultSecurityFilterChain defined as 'apiFilterChain' in [class path resource [com/ankurm/jwtauth/config/SecurityConfig.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, JwtAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, SessionManagement, ExceptionTranslation, Authorization]"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 20. SecurityContext across a thread boundary.
|
||||
|
||||
HTTP 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"onRequestThread": "root",
|
||||
"onPlainExecutor": "null (context did not cross the thread)",
|
||||
"onDelegatingExecutor": "root"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# end of transcript
|
||||
40
docs/output/expiry-and-clock-skew.txt
Normal file
40
docs/output/expiry-and-clock-skew.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - token expiry and the 60-second clock skew
|
||||
profiles: hs256,shortlived (access-token-ttl = 2s)
|
||||
==========================================================================
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# T+0s - fresh token
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"name": "alice",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER",
|
||||
"ROLE_USER",
|
||||
"SCOPE_profile:read"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# T+5s - exp has passed, but JwtTimestampValidator allows 60s of clock skew
|
||||
# by default, so the token is STILL accepted. This surprises people
|
||||
# who write a test that sleeps past exp and expects a 401.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"name": "alice",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER",
|
||||
"ROLE_USER",
|
||||
"SCOPE_profile:read"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# T+65s - past exp + the 60s skew window. Now it is refused.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Jwt expired at 2026-08-22T06:16:50Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# end
|
||||
63
docs/output/resource-server-loose.txt
Normal file
63
docs/output/resource-server-loose.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - built-in resource server
|
||||
profiles: hs256,resourceserver
|
||||
==========================================================================
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 1. The filter chain. Note BearerTokenAuthenticationFilter in place of our
|
||||
# hand-written JwtAuthenticationFilter - same slot, framework-owned.
|
||||
|
||||
HTTP 200
|
||||
[
|
||||
{
|
||||
"matchesThisRequest": true,
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"LogoutFilter",
|
||||
"OAuth2ProtectedResourceMetadataFilter",
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 2. Access token -> 200.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"name": "alice",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER",
|
||||
"ROLE_USER",
|
||||
"SCOPE_profile:read"
|
||||
],
|
||||
"authenticationType": "JwtAuthenticationToken",
|
||||
"jti": "5dc2e769-9413-44e4-9bcb-6ab9fe1f6b2e",
|
||||
"issuer": "https://jwt-auth-demo.ankurm.com",
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 3. Non-admin on an admin route -> 403 insufficient_scope.
|
||||
|
||||
HTTP 403
|
||||
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 4. REFRESH token presented as an access token.
|
||||
# This is the line to watch when comparing the two runs.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"name": "alice",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER"
|
||||
],
|
||||
"authenticationType": "JwtAuthenticationToken",
|
||||
"jti": "8e069702-70a3-4029-89e8-d03c8b3e01ce",
|
||||
"issuer": "https://jwt-auth-demo.ankurm.com",
|
||||
"audience": [
|
||||
"jwt-auth-demo-api"
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# end
|
||||
53
docs/output/resource-server-strict.txt
Normal file
53
docs/output/resource-server-strict.txt
Normal file
@@ -0,0 +1,53 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - built-in resource server
|
||||
profiles: hs256,resourceserver,strict
|
||||
==========================================================================
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 1. The filter chain. Note BearerTokenAuthenticationFilter in place of our
|
||||
# hand-written JwtAuthenticationFilter - same slot, framework-owned.
|
||||
|
||||
HTTP 200
|
||||
[
|
||||
{
|
||||
"chain": "DefaultSecurityFilterChain defined as 'apiFilterChain' in [class path resource [com/ankurm/jwtauth/config/ResourceServerSecurityConfig.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Logout, OAuth2ProtectedResourceMetadata, BearerTokenAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, SessionManagement, ExceptionTranslation, Authorization]",
|
||||
"filters": [
|
||||
"DisableEncodeUrlFilter",
|
||||
"WebAsyncManagerIntegrationFilter",
|
||||
"SecurityContextHolderFilter",
|
||||
"HeaderWriterFilter",
|
||||
"LogoutFilter",
|
||||
"OAuth2ProtectedResourceMetadataFilter",
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 2. Access token -> 200.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"name": "alice",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER",
|
||||
"ROLE_USER",
|
||||
"SCOPE_profile:read"
|
||||
],
|
||||
"authenticationType": "JwtAuthenticationToken",
|
||||
"jti": "c140fc47-8b48-41e9-bd01-071d398b6c8b",
|
||||
"issuer": "https://jwt-auth-demo.ankurm.com",
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 3. Non-admin on an admin route -> 403 insufficient_scope.
|
||||
|
||||
HTTP 403
|
||||
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 4. REFRESH token presented as an access token.
|
||||
# This is the line to watch when comparing the two runs.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Expected a token with token_type=access", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# end
|
||||
83
docs/output/rs256-demo.txt
Normal file
83
docs/output/rs256-demo.txt
Normal file
@@ -0,0 +1,83 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - RS256 variant
|
||||
profiles: rs256 (private key signs, public key / JWKS verifies)
|
||||
==========================================================================
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 1. The public half, published as a JWK Set. No private material here -
|
||||
# n and e only. Any number of resource servers can poll this.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"e": "AQAB",
|
||||
"kid": "demo-rsa-2026-08",
|
||||
"n": "5NEDQPQW0Gz6iR5-UNl7J7660_Psd5q1f5VamK9KTS9f6YhPPIG8mfi6zWe8XmxxdR_Bd2yaX-v_Wz6MgeFLbBVkRFfve_zVnq4-kgjhn8UaRK1iU0C1j-7SahD73hHqGaOjAlFNro5ygjGAcVL8RGVMxRMy4aaTAm3KB4EdG2hJFxyfCBqtkwsHTM_DXcoFTLTZ2bI-hPhN6uBxk7ykFaCnQ47yrKSM6kn0ul0dp22AK_4mP1SRDnnr3Da5GFhMKtBqy_GgXcJ9WTpxIYhlr8B5modtb5S34900VPScpoXiJdUhghxEpXbb1W_PlpVIElISHzldnwWOogdPncO0zQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 2. Login. Same endpoint, same request, different signature algorithm.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"accessToken": "eyJraWQiOiJkZW1vLXJzYS0yMDI2LTA4IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJyb290IiwiYXVkIjoiand0LWF1dGgtZGVtby1hcGkiLCJuYmYiOjE3ODczNzkzOTEsInNjb3BlIjoiYWRtaW46cmVhZCBwcm9maWxlOnJlYWQiLCJyb2xlcyI6WyJBRE1JTiIsIlVTRVIiXSwiaXNzIjoiaHR0cHM6Ly9qd3QtYXV0aC1kZW1vLmFua3VybS5jb20iLCJleHAiOjE3ODczODAyOTEsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJpYXQiOjE3ODczNzkzOTEsImp0aSI6IjIwZWZiMmRhLTkwYWEtNGZjYy04ZTU1LWI0NjhkNDAxMWUwYSJ9.0sPi-ArszI-wKbMuZes2bUCQbi3b68hyNngUPohzrKhWgYlHThu_JIq6gIFcYhq6qK1UYrL2c2lI6uxflSHjdtl5vRhvOUHlDc63eDtzQFIMnC9-kitwwi_x5pV09IxYBVRo38K5WkD9uiIYnNoAenNLhoAfAV424464oi2X_XtsPlXdrhUjCFl8nLghAZSsDaVvTFW9PKHtWfdJUZht2vcZI54TaNFFQKIjAOjTObAwFg9kkzXzcoNiH3wBYKXe4hy3IRDxi62Zl0-eVA67a-ODGqbsPn3BVMbaaLN8iX3OKZtBAUTpYNQQ9StkBfPds5PrxbL_i9EI-j2GpXzA8Q",
|
||||
"refreshToken": "eyJraWQiOiJkZW1vLXJzYS0yMDI2LTA4IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJyb290IiwiYXVkIjoiand0LWF1dGgtZGVtby1hcGkiLCJuYmYiOjE3ODczNzkzOTEsImlzcyI6Imh0dHBzOi8vand0LWF1dGgtZGVtby5hbmt1cm0uY29tIiwiZXhwIjoxNzg3NDA4MTkxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImlhdCI6MTc4NzM3OTM5MSwianRpIjoiMjdiOGVhZmMtMGY1NS00YTQ3LWJlYjMtNjJlZDlkOTFmYzgzIn0.pnT-xsAMa-4a1VdUA3_98dGYD-6A7wYKOGUCdmnWbBL44JT2D159w4LASgAAr41Vd7onzlbCgK-cIXlscLmsDSm3Qo0xTO9VjeLYkmFByfSHM7YnG1ecZCuv6PXcqGvpTY_VT35oou0J5qpAl3pBIbkI9mtlI-mBj4ecnlZAEewki_gatloopkDUhg_5xitCeYZfZhB1nvEyCHcv42hpvGHUDBVtebNo6m7rVSpgr7BfVSyAi057qZR0m0Ddm-oJk252vYIQWu9ZH3QE15bMbP7wKOPHfeX1VfuaaUh_o9EgIEblaW1rD9MhupjokUhd2G5OfEbBA_4EDEIB3SpHWg",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 900
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 3. The JOSE header now carries alg=RS256 and the kid that selects the key.
|
||||
|
||||
{
|
||||
"kid": "demo-rsa-2026-08",
|
||||
"typ": "JWT",
|
||||
"alg": "RS256"
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 4. Token length. RS256 signatures are 256 bytes; HS256 signatures are 32.
|
||||
|
||||
RS256 access token: 758 characters
|
||||
signature segment : 343 characters
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 5. It works exactly the same from the caller's side.
|
||||
|
||||
HTTP 200
|
||||
{
|
||||
"name": "root",
|
||||
"authorities": [
|
||||
"FACTOR_BEARER",
|
||||
"ROLE_ADMIN",
|
||||
"ROLE_USER",
|
||||
"SCOPE_admin:read",
|
||||
"SCOPE_profile:read"
|
||||
],
|
||||
"authenticationType": "JwtAuthenticationToken",
|
||||
"jti": "20efb2da-90aa-4fcc-8e55-b468d4011e0a",
|
||||
"issuer": "https://jwt-auth-demo.ankurm.com",
|
||||
"audience": [
|
||||
"jwt-auth-demo-api"
|
||||
],
|
||||
"issuedAt": "2026-08-22T06:16:31Z",
|
||||
"expiresAt": "2026-08-22T06:31:31Z",
|
||||
"algorithm": "RS256",
|
||||
"keyId": "demo-rsa-2026-08"
|
||||
}
|
||||
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# 6. Tampered payload, original signature -> 401, same as HS256.
|
||||
|
||||
HTTP 401
|
||||
WWW-Authenticate: Bearer realm="jwt-auth-demo", error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource"
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
# end
|
||||
14
docs/output/test-run.txt
Normal file
14
docs/output/test-run.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
==========================================================================
|
||||
jwt-auth-demo - test run
|
||||
==========================================================================
|
||||
|
||||
Running com.ankurm.jwtauth.AuthenticationFlowTests
|
||||
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.134 s -- in com.ankurm.jwtauth.AuthenticationFlowTests
|
||||
Running com.ankurm.jwtauth.CsrfBreaksPermitAllTests
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.014 s -- in com.ankurm.jwtauth.CsrfBreaksPermitAllTests
|
||||
Tests run: 13, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
|
||||
JDK : openjdk version "25.0.4.1" 2026-08-18 LTS (Temurin 25.0.4.1+1)
|
||||
Boot : 4.1.1
|
||||
Security : 7.1.1
|
||||
Reference in New Issue
Block a user