1
0
Files
Ankur Mhatre 4dc45d5e00 Add OAuth2 resource server project: JWT validation, JWKS and key rotation
Companion code for the follow-up article. The repository now holds two Maven
projects sharing one docs/ tree:

  jwt-authentication/       the hand-written filter application (unchanged, moved)
  oauth2-resource-server/   a resource server, a Keycloak compose, and a stub
                            issuer whose JWK Set can be mutated on command

The stub exists because Keycloak will not rotate a signing key at a chosen
second, report how many times its JWKS endpoint was fetched, or drop a key from
the published set on request - and the caching and rotation measurements need
all three. The Keycloak run confirms the same code path against a real issuer.

Findings captured under docs/output/, all from real runs:

  * The default validator stack does not check aud. A token minted for another
    service in the same realm is accepted.
  * Spring Security builds its JWKSource with refreshAheadCache(false) and
    rateLimited(false), overriding two of Nimbus's protective defaults, and
    enables Nimbus caching only when NO Spring cache was supplied - so
    supplying one removes the five-minute expiry.
  * A key retired from the JWK Set stops being accepted at t+300s with the
    default cache, and never with a Spring cache that has no TTL.
  * 25 tokens carrying an unknown kid produce 25 JWKS fetches at the issuer,
    through permitAll() endpoints included.
  * A hyphenated client id in an authorities-claim-expression parses as
    subtraction; the SpelEvaluationException is swallowed and logged at TRACE.
  * A clientScopes key in a Keycloak realm import replaces the built-in scopes
    rather than adding to them.

New docs chapters 12-18. README covers both projects. Existing docs and scripts
updated for the new paths; no docs/output/ file from the first article moved, so
links in the published article still resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013f7f2XZXrQ6gW3RtZE187t
2026-08-23 11:00:56 +00:00

101 lines
4.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regenerates every ../docs/output/rs-*.txt file from a real run.
#
# Needs Docker for the Keycloak leg. Takes roughly twenty-five minutes, most of it spent
# restarting the resource server between profile sets and waiting out cache lifetimes.
set -eu
cd "$(dirname "$0")/.."
OUT=../docs/output
mkdir -p "$OUT"
echo "==> tests"
{
echo "=========================================================================="
echo " oauth2-resource-server-demo - test run"
echo "=========================================================================="
echo
mvn -B test 2>&1 | grep -E '^\[INFO\] (Running|Tests run)|^\[ERROR\]|BUILD (SUCCESS|FAILURE)' \
| sed 's/^\[INFO\] //'
echo
echo "JDK : $(java -version 2>&1 | grep -v 'JAVA_TOOL\|Picked up' | head -1)"
echo "Boot : 4.1.1"
echo "Security : 7.1.1"
echo "Nimbus : 10.9.1"
} > "$OUT/rs-test-run.txt"
echo "==> stub issuer"
./scripts/run-stub-issuer.sh >/dev/null
echo "==> issuer and audience validation"
./scripts/run-rs.sh stub,roles,audience >/dev/null
./scripts/issuer-audience-demo.sh "stub,roles,audience" > "$OUT/rs-issuer-audience.txt" 2>&1
echo "==> the same run with a type validator that accepts at+jwt"
./scripts/run-rs.sh stub,roles,audience,attyp >/dev/null
./scripts/issuer-audience-demo.sh "stub,roles,audience,attyp" > "$OUT/rs-issuer-audience-attyp.txt" 2>&1
echo "==> authorities: the default converter"
./scripts/run-rs.sh stub >/dev/null
./scripts/converter-demo.sh "stub" > "$OUT/rs-converter-default.txt" 2>&1
echo "==> authorities: a custom JwtAuthenticationConverter"
./scripts/run-rs.sh stub,roles >/dev/null
./scripts/converter-demo.sh "stub,roles" > "$OUT/rs-converter-java.txt" 2>&1
echo "==> authorities: configuration only"
./scripts/run-rs.sh stub,propsroles >/dev/null
./scripts/converter-demo.sh "stub,propsroles" > "$OUT/rs-converter-properties.txt" 2>&1
echo "==> authorities: the same configuration with an unquoted SpEL indexer"
./scripts/run-rs.sh stub,propsroles-broken,tracespel >/dev/null
{
./scripts/converter-demo.sh "stub,propsroles-broken,tracespel"
echo
echo "--------------------------------------------------------------------------"
echo " what the resource server logged, at TRACE, while producing that 403"
echo "--------------------------------------------------------------------------"
grep -F 'ExpressionJwtGrantedAuthoritiesConverter' /tmp/rs-stub-propsroles-broken-tracespel.log \
| sed 's/^.*ExpressionJwtGrantedAuthoritiesConverter *: / /' | sort -u
} > "$OUT/rs-converter-properties-broken.txt" 2>&1
echo "==> the live JWK source chain, three cache configurations"
rm -f "$OUT/rs-decoder-chain.txt"
for P in stub stub,springcache stub,nottlcache stub,hardened; do
./scripts/run-rs.sh "$P" >/dev/null
./scripts/decoder-chain.sh "$P" >> "$OUT/rs-decoder-chain.txt" 2>&1
done
echo "==> rotation, from a cold resource server"
./scripts/run-stub-issuer.sh >/dev/null
./scripts/run-rs.sh stub,roles,audience >/dev/null
./scripts/rotation-demo.sh "stub,roles,audience" > "$OUT/rs-rotation.txt" 2>&1
echo "==> what an unknown kid costs the issuer"
./scripts/run-stub-issuer.sh >/dev/null
./scripts/run-rs.sh stub,roles >/dev/null
./scripts/amplification-demo.sh "stub,roles" 25 > "$OUT/rs-jwks-amplification.txt" 2>&1
echo "==> a retired key, default caching (takes ~8 minutes)"
./scripts/run-stub-issuer.sh >/dev/null
./scripts/run-rs.sh stub,roles >/dev/null
./scripts/retired-key-demo.sh "stub,roles" 16 30 > "$OUT/rs-retired-key-default.txt" 2>&1
echo "==> a retired key, Spring cache with no TTL (takes ~8 minutes)"
./scripts/run-stub-issuer.sh >/dev/null
./scripts/run-rs.sh stub,roles,nottlcache >/dev/null
./scripts/retired-key-demo.sh "stub,roles,nottlcache" 16 30 > "$OUT/rs-retired-key-nottlcache.txt" 2>&1
echo "==> Keycloak"
docker compose -f docker/compose.yaml up -d >/dev/null 2>&1
for i in $(seq 1 60); do
curl -sf http://localhost:8080/realms/demo/.well-known/openid-configuration -o /dev/null 2>/dev/null && break
sleep 3
done
./scripts/run-rs.sh keycloak,roles >/dev/null
./scripts/keycloak-demo.sh > "$OUT/rs-keycloak.txt" 2>&1
./scripts/run-rs.sh keycloak >/dev/null
./scripts/keycloak-demo.sh > "$OUT/rs-keycloak-default-converter.txt" 2>&1
for p in $(ps -eo pid,cmd | grep -E '[R]esourceServerApplication|[S]tubIssuerApplication' | awk '{print $1}'); do kill -9 "$p" || true; done
echo "==> done. docs/output/rs-*.txt regenerated."