==========================================================================
 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
