#!/usr/bin/env bash # Regenerates docs/output/curl-transcript.txt against a running instance. # Usage: ./scripts/curl-transcript.sh [base-url] set -u BASE="${1:-http://localhost:8080}" hr() { printf '\n%s\n' "--------------------------------------------------------------------------"; } step(){ hr; printf '# %s\n\n' "$1"; } # Print status line, the security-relevant headers, and the body. show() { local out out=$(curl -sS -D /tmp/.h -o /tmp/.b -w '%{http_code}' "$@") printf 'HTTP %s\n' "$out" grep -iE '^(www-authenticate|content-type|set-cookie):' /tmp/.h | sed 's/\r$//' if [ -s /tmp/.b ]; then python3 -m json.tool < /tmp/.b 2>/dev/null || cat /tmp/.b echo fi } jwt_part() { # $1=token $2=0|1 -> pretty-print header or payload echo "$1" | cut -d. -f$(( $2 + 1 )) \ | tr '_-' '/+' | sed 's/$/==/' | base64 -d 2>/dev/null \ | python3 -m json.tool 2>/dev/null } echo "==========================================================================" echo " jwt-auth-demo - curl transcript" echo " target : $BASE" echo "==========================================================================" step "1. Public endpoint, no token. permitAll() means the filter chain lets it through." show "$BASE/api/public/ping" step "2. Protected endpoint, no token. 401 - we do not know who you are." show "$BASE/api/me" step "3. Wrong password. Still 401, and the body says nothing about which half was wrong." show -X POST "$BASE/api/auth/login" -H 'Content-Type: application/json' \ -d '{"username":"alice","password":"wrong-password"}' step "4. Locked account. 401 with the identical body - no account-state oracle." show -X POST "$BASE/api/auth/login" -H 'Content-Type: application/json' \ -d '{"username":"locked","password":"locked-password"}' step "5. Login as alice (ROLE_USER, SCOPE_profile:read)." show -X POST "$BASE/api/auth/login" -H 'Content-Type: application/json' \ -d '{"username":"alice","password":"alice-password"}' ALICE=$(python3 -c "import json;print(json.load(open('/tmp/.b'))['accessToken'])") ALICE_REFRESH=$(python3 -c "import json;print(json.load(open('/tmp/.b'))['refreshToken'])") step "6. What is actually inside that token (base64url decode - no signature check)." echo "--- JOSE header ---" jwt_part "$ALICE" 0 echo "--- claims ---" jwt_part "$ALICE" 1 step "7. The same protected endpoint, now with the token. 200." show "$BASE/api/me" -H "Authorization: Bearer $ALICE" step "8. alice hits an ADMIN endpoint. 403, not 401 - we know who she is, she may not." show "$BASE/api/admin/stats" -H "Authorization: Bearer $ALICE" step "9. Same request with a scope-based rule (@PreAuthorize). Also 403." show "$BASE/api/reports" -H "Authorization: Bearer $ALICE" step "10. Login as root and repeat. 200." show -X POST "$BASE/api/auth/login" -H 'Content-Type: application/json' \ -d '{"username":"root","password":"root-password"}' ROOT=$(python3 -c "import json;print(json.load(open('/tmp/.b'))['accessToken'])") show "$BASE/api/admin/stats" -H "Authorization: Bearer $ROOT" step "11. Tampered payload, original signature. 401 invalid_token." HDR=$(echo "$ALICE" | cut -d. -f1); SIG=$(echo "$ALICE" | cut -d. -f3) FORGED_PAYLOAD=$(echo "$ALICE" | cut -d. -f2 | tr '_-' '/+' | sed 's/$/==/' | base64 -d 2>/dev/null \ | sed 's/"roles":\["USER"\]/"roles":["ADMIN"]/' | base64 -w0 | tr '/+' '_-' | tr -d '=') show "$BASE/api/admin/stats" -H "Authorization: Bearer $HDR.$FORGED_PAYLOAD.$SIG" step "12. Garbage where a token should be. 401, and note it is NOT 400." show "$BASE/api/me" -H "Authorization: Bearer not-a-jwt" step "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." show "$BASE/api/me" -H "Authorization: $ALICE" step "14. A refresh token presented as an access token. 401 - the token_type claim." show "$BASE/api/me" -H "Authorization: Bearer $ALICE_REFRESH" step "15. Refresh with rotation. New access token, new refresh token." show -X POST "$BASE/api/auth/refresh" -H 'Content-Type: application/json' \ -d "{\"refreshToken\":\"$ALICE_REFRESH\"}" ALICE2=$(python3 -c "import json;print(json.load(open('/tmp/.b'))['accessToken'])") step "16. Replay the spent refresh token. 401 - rotation makes replay detectable." show -X POST "$BASE/api/auth/refresh" -H 'Content-Type: application/json' \ -d "{\"refreshToken\":\"$ALICE_REFRESH\"}" step "17. Logout revokes the presented access token by its jti." show -X POST "$BASE/api/auth/logout" -H "Authorization: Bearer $ALICE2" show "$BASE/api/auth/revocations" -H "Authorization: Bearer $ROOT" step "18. The revoked token, still cryptographically valid, is now refused." show "$BASE/api/me" -H "Authorization: Bearer $ALICE2" step "19. The real filter order, read from FilterChainProxy at runtime." show "$BASE/api/public/filters" step "20. SecurityContext across a thread boundary." show "$BASE/api/async-demo" -H "Authorization: Bearer $ROOT" hr echo "# end of transcript"