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
This commit is contained in:
58
oauth2-resource-server/scripts/amplification-demo.sh
Executable file
58
oauth2-resource-server/scripts/amplification-demo.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# What an unknown kid costs the issuer.
|
||||
#
|
||||
# Spring Security builds its JWKSource with rateLimited(false), overriding Nimbus's own
|
||||
# default of a 30-second minimum interval between forced refreshes. Nothing then stands
|
||||
# between a token bearing an unrecognised kid and an HTTP request to the issuer.
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
PROFILES="${1:-unknown}"
|
||||
N="${2:-25}"
|
||||
|
||||
head1 "resource server profiles: $PROFILES"
|
||||
|
||||
echo "Baseline: $N requests with a VALID token, whose kid is in the cached JWK Set."
|
||||
curl -s -X POST "$STUB/admin/reset-counter" >/dev/null
|
||||
GOOD=$(stub_token "sub=alice&aud=reports-api")
|
||||
curl -s -o /dev/null -H "Authorization: Bearer $GOOD" "$RS/api/me" # warm
|
||||
curl -s -X POST "$STUB/admin/reset-counter" >/dev/null
|
||||
for i in $(seq 1 "$N"); do
|
||||
curl -s -o /dev/null -H "Authorization: Bearer $GOOD" "$RS/api/me"
|
||||
done
|
||||
echo " requests to the resource server : $N"
|
||||
echo " fetches of /jwks.json : $(stub_fetches)"
|
||||
|
||||
echo
|
||||
echo "Now $N requests carrying a token whose kid has never existed."
|
||||
echo "Each one is refused - but look at what it costs the issuer first."
|
||||
curl -s -X POST "$STUB/admin/reset-counter" >/dev/null
|
||||
for i in $(seq 1 "$N"); do
|
||||
BAD=$(curl -s -X POST "$STUB/token/unknown-kid")
|
||||
curl -s -o /dev/null -H "Authorization: Bearer $BAD" "$RS/api/me"
|
||||
done
|
||||
FETCHES=$(stub_fetches)
|
||||
echo " requests to the resource server : $N"
|
||||
echo " fetches of /jwks.json : $FETCHES"
|
||||
echo
|
||||
echo "Every rejected request became a request to the authorization server. An attacker who"
|
||||
echo "can reach an unauthenticated endpoint of your resource server can point that ratio at"
|
||||
echo "your identity provider, from one connection, using tokens that are never valid."
|
||||
echo
|
||||
echo "And it does not need an authenticated endpoint. /api/public/ping is permitAll()."
|
||||
curl -s -X POST "$STUB/admin/reset-counter" >/dev/null
|
||||
for i in $(seq 1 "$N"); do
|
||||
BAD=$(curl -s -X POST "$STUB/token/unknown-kid")
|
||||
curl -s -o /dev/null -H "Authorization: Bearer $BAD" "$RS/api/public/ping"
|
||||
done
|
||||
echo " requests to /api/public/ping : $N"
|
||||
echo " fetches of /jwks.json : $(stub_fetches)"
|
||||
echo
|
||||
echo "A permitAll() endpoint still evaluates a bearer token if one is present, because"
|
||||
echo "BearerTokenAuthenticationFilter runs before any authorization rule. Presenting a"
|
||||
echo "broken token to a public endpoint gets a 401 from the public endpoint - and a"
|
||||
echo "JWKS fetch on the way."
|
||||
call "GET /api/public/ping with an unknown kid" "$RS/api/public/ping" "$(curl -s -X POST "$STUB/token/unknown-kid")"
|
||||
echo
|
||||
echo "The status returned to the caller is unremarkable:"
|
||||
BAD=$(curl -s -X POST "$STUB/token/unknown-kid")
|
||||
call "GET /api/me with an unknown kid" "$RS/api/me" "$BAD"
|
||||
20
oauth2-resource-server/scripts/converter-demo.sh
Executable file
20
oauth2-resource-server/scripts/converter-demo.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# What a token is allowed to do, and why Keycloak's roles are invisible by default.
|
||||
# Pass the profile set the resource server is running with, for the transcript header.
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
PROFILES="${1:-unknown}"
|
||||
|
||||
head1 "resource server profiles: $PROFILES"
|
||||
|
||||
head1 "1. alice - realm role USER, client role reports-reader"
|
||||
T=$(stub_token "sub=alice&roles=USER")
|
||||
claims "$T"
|
||||
call "GET /api/me (which authorities did the converter produce?)" "$RS/api/me" "$T"
|
||||
call "GET /api/reports (needs ROLE_reports-reader, from resource_access)" "$RS/api/reports" "$T"
|
||||
call "GET /api/admin/stats (needs ROLE_ADMIN, from realm_access)" "$RS/api/admin/stats" "$T"
|
||||
|
||||
head1 "2. root - realm roles USER and ADMIN"
|
||||
T=$(stub_token "sub=root&roles=USER%20ADMIN")
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
call "GET /api/admin/stats" "$RS/api/admin/stats" "$T"
|
||||
9
oauth2-resource-server/scripts/decoder-chain.sh
Executable file
9
oauth2-resource-server/scripts/decoder-chain.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prints the JWK source chain the running decoder actually has.
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
head1 "resource server profiles: ${1:-unknown}"
|
||||
echo "One token is decoded first, because the decoder for an issuer-uri is built lazily."
|
||||
T=$(stub_token "sub=alice&aud=reports-api" 2>/dev/null || true)
|
||||
[ -n "${T:-}" ] && curl -s -o /dev/null -H "Authorization: Bearer $T" "$RS/api/me" || true
|
||||
curl -s "$RS/api/public/decoder" | python3 -m json.tool
|
||||
51
oauth2-resource-server/scripts/issuer-audience-demo.sh
Executable file
51
oauth2-resource-server/scripts/issuer-audience-demo.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# The claim checks that decide whether a correctly signed token is yours.
|
||||
# Usage: ./scripts/issuer-audience-demo.sh "<profiles the resource server is running with>"
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
PROFILES="${1:-unknown}"
|
||||
|
||||
head1 "resource server profiles: $PROFILES"
|
||||
|
||||
head1 "1. A correct token"
|
||||
T=$(stub_token "sub=alice&aud=reports-api")
|
||||
claims "$T"
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
|
||||
head1 "2. Signed by the right key, but iss says something else"
|
||||
echo "The signature verifies. The key is the same key. Only the string differs."
|
||||
T=$(stub_token "sub=alice&aud=reports-api&issuerOverride=http://localhost:9000/other")
|
||||
claims "$T"
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
|
||||
head1 "3. A token minted for a different service in the same realm"
|
||||
echo "This is the one that silently works when nothing checks aud."
|
||||
T=$(stub_token "sub=alice&aud=billing-api")
|
||||
claims "$T"
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
|
||||
head1 "4. Expired 90 seconds ago"
|
||||
echo "The default clock skew is 60s, so a token has to be more than a minute stale"
|
||||
echo "before JwtTimestampValidator refuses it."
|
||||
T=$(stub_token "sub=alice&aud=reports-api&issuedAgoSeconds=120&expiresInSeconds=30")
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
|
||||
head1 "5. Expired 30 seconds ago - inside the default clock skew"
|
||||
T=$(stub_token "sub=alice&aud=reports-api&issuedAgoSeconds=60&expiresInSeconds=30")
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
|
||||
head1 "6. typ=at+jwt, which RFC 9068 says an access token SHOULD carry"
|
||||
echo "The default validator stack contains JwtTypeValidator.jwt(), which accepts only an"
|
||||
echo "absent typ or typ=JWT. Whether this passes depends on the attyp profile."
|
||||
T=$(stub_token "sub=alice&aud=reports-api&typ=at%2Bjwt")
|
||||
claims "$T"
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
|
||||
head1 "7. No token at all"
|
||||
call "GET /api/me" "$RS/api/me"
|
||||
call "GET /api/public/ping" "$RS/api/public/ping"
|
||||
|
||||
head1 "8. The endpoint nobody configured"
|
||||
echo "Spring Security 7 publishes RFC 9728 protected resource metadata and points the"
|
||||
echo "WWW-Authenticate challenge at it. It answers without a token."
|
||||
call "GET /.well-known/oauth-protected-resource" "$RS/.well-known/oauth-protected-resource"
|
||||
35
oauth2-resource-server/scripts/keycloak-demo.sh
Executable file
35
oauth2-resource-server/scripts/keycloak-demo.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# The same resource server, the same code, a real Keycloak.
|
||||
# Requires: docker compose -f docker/compose.yaml up -d
|
||||
# ./scripts/run-rs.sh keycloak,roles
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
head1 "Keycloak discovery document"
|
||||
curl -s "$KC/realms/demo/.well-known/openid-configuration" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); [print(" %-22s %s" % (k, d[k])) for k in ("issuer","jwks_uri","token_endpoint")]'
|
||||
|
||||
head1 "Keycloak JWK Set"
|
||||
curl -s "$KC/realms/demo/protocol/openid-connect/certs" \
|
||||
| python3 -c '
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print(" keys published:", len(d["keys"]))
|
||||
for k in d["keys"]:
|
||||
print(" kid=%s alg=%s use=%s kty=%s" % (k.get("kid"), k.get("alg"), k.get("use"), k.get("kty")))'
|
||||
|
||||
head1 "1. alice, password grant"
|
||||
T=$(kc_token alice alice-password)
|
||||
claims "$T"
|
||||
call "GET /api/me" "$RS/api/me" "$T"
|
||||
call "GET /api/reports (client role, from resource_access.reports-api.roles)" "$RS/api/reports" "$T"
|
||||
call "GET /api/admin/stats (realm role ADMIN, which alice does not have)" "$RS/api/admin/stats" "$T"
|
||||
|
||||
head1 "2. root"
|
||||
T=$(kc_token root root-password)
|
||||
call "GET /api/admin/stats" "$RS/api/admin/stats" "$T"
|
||||
|
||||
head1 "3. nobody - a user with no client role"
|
||||
T=$(kc_token nobody nobody-password)
|
||||
claims "$T"
|
||||
call "GET /api/reports" "$RS/api/reports" "$T"
|
||||
50
oauth2-resource-server/scripts/lib.sh
Executable file
50
oauth2-resource-server/scripts/lib.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers. Sourced, not run.
|
||||
RS="${RS:-http://localhost:8081}"
|
||||
STUB="${STUB:-http://localhost:9000}"
|
||||
KC="${KC:-http://localhost:8080}"
|
||||
|
||||
hr() { printf '%s\n' "--------------------------------------------------------------------------"; }
|
||||
head1(){ hr; printf ' %s\n' "$1"; hr; }
|
||||
|
||||
# Prints status, the RFC 6750 challenge header, and the body. The WWW-Authenticate header is
|
||||
# the only place a claim-validation failure explains itself, so it is never omitted here.
|
||||
call() {
|
||||
local label="$1" url="$2" token="${3:-}"
|
||||
printf '\n$ %s\n' "$label"
|
||||
local args=(-s -o /tmp/.body -D /tmp/.hdr -w '%{http_code}')
|
||||
[ -n "$token" ] && args+=(-H "Authorization: Bearer $token")
|
||||
local code
|
||||
code=$(curl "${args[@]}" "$url")
|
||||
printf 'HTTP %s\n' "$code"
|
||||
grep -i '^www-authenticate:' /tmp/.hdr | sed 's/\r$//' || true
|
||||
if [ -s /tmp/.body ]; then
|
||||
python3 -m json.tool < /tmp/.body 2>/dev/null || cat /tmp/.body
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
stub_token() { curl -s -X POST "$STUB/token?$1"; }
|
||||
stub_state() { curl -s "$STUB/admin/state" | python3 -m json.tool; }
|
||||
stub_fetches() { curl -s "$STUB/admin/state" | python3 -c 'import json,sys;print(json.load(sys.stdin)["jwksFetches"])'; }
|
||||
|
||||
kc_token() {
|
||||
curl -s -X POST "$KC/realms/demo/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=demo-client -d client_secret=demo-secret \
|
||||
-d "username=$1" -d "password=$2" \
|
||||
| python3 -c 'import json,sys;print(json.load(sys.stdin).get("access_token",""))'
|
||||
}
|
||||
|
||||
claims() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys, base64, json
|
||||
tok = sys.argv[1]
|
||||
h, p, _ = tok.split('.')
|
||||
pad = lambda s: s + '=' * (-len(s) % 4)
|
||||
print(" header:", json.dumps(json.loads(base64.urlsafe_b64decode(pad(h)))))
|
||||
c = json.loads(base64.urlsafe_b64decode(pad(p)))
|
||||
for k in ("iss", "aud", "typ", "scope", "preferred_username", "realm_access", "resource_access"):
|
||||
if k in c:
|
||||
print(" %-18s %s" % (k, json.dumps(c[k])))
|
||||
PY
|
||||
}
|
||||
56
oauth2-resource-server/scripts/retired-key-demo.sh
Executable file
56
oauth2-resource-server/scripts/retired-key-demo.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# How long a key stays trusted after the issuer removes it from the JWK Set.
|
||||
#
|
||||
# The only traffic after the retirement is the leaked token itself. That is the point:
|
||||
# a token whose kid IS in the cached set never triggers the unknown-kid refresh, so the
|
||||
# only thing that can dislodge the stale JWK Set is the cache expiring on its own.
|
||||
#
|
||||
# Run under both cache configurations and diff the transcripts:
|
||||
# ./scripts/run-rs.sh stub,roles && ./scripts/retired-key-demo.sh "stub,roles"
|
||||
# ./scripts/run-rs.sh stub,roles,nottlcache && ./scripts/retired-key-demo.sh "stub,roles,nottlcache"
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
PROFILES="${1:-unknown}"
|
||||
PROBES="${2:-16}"
|
||||
INTERVAL="${3:-30}"
|
||||
|
||||
probe() { curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $1" "$RS/api/me"; }
|
||||
|
||||
head1 "resource server profiles: $PROFILES"
|
||||
curl -s -X POST "$STUB/admin/reset-counter" >/dev/null
|
||||
|
||||
echo "Scenario: a signing key is compromised. The issuer publishes a replacement and"
|
||||
echo "removes the compromised key from the JWK Set immediately. Tokens it signed are"
|
||||
echo "already out there with an hour left to run."
|
||||
echo
|
||||
|
||||
VICTIM_KID=$(curl -s "$STUB/admin/state" | python3 -c 'import json,sys;print(json.load(sys.stdin)["activeKid"])')
|
||||
LEAKED=$(stub_token "sub=attacker&aud=reports-api&expiresInSeconds=3600")
|
||||
echo "1. A token signed with $VICTIM_KID, one hour to live: GET /api/me -> $(probe "$LEAKED")"
|
||||
echo " jwks fetches: $(stub_fetches)"
|
||||
echo
|
||||
|
||||
NEW=$(curl -s -X POST "$STUB/admin/publish" | python3 -c 'import json,sys;print(json.load(sys.stdin)["publishedKids"][-1])')
|
||||
curl -s -X POST "$STUB/admin/activate?kid=$NEW" >/dev/null
|
||||
curl -s -X POST "$STUB/admin/retire?kid=$VICTIM_KID" >/dev/null
|
||||
echo "2. Issuer rotates to $NEW and retires $VICTIM_KID."
|
||||
stub_state
|
||||
echo
|
||||
echo " Anyone fetching /jwks.json from this moment sees only $NEW."
|
||||
echo
|
||||
|
||||
echo "3. From here the ONLY traffic is the leaked token. Nothing carries an unknown kid,"
|
||||
echo " so nothing forces a refresh. Whether the token keeps working is decided purely"
|
||||
echo " by whether the cached JWK Set expires."
|
||||
echo
|
||||
printf ' %-10s %-8s %s\n' "elapsed" "leaked" "jwksFetches"
|
||||
START=$(date +%s)
|
||||
for i in $(seq 1 "$PROBES"); do
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
printf ' t+%-8s %-8s %s\n' "${ELAPSED}s" "$(probe "$LEAKED")" "$(stub_fetches)"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
echo
|
||||
echo "A row that flips to 401 is the cache expiring and the retired key going away."
|
||||
echo "A column of 200s is a resource server that has not noticed, and will not, until"
|
||||
echo "something happens to bring it a token it cannot verify."
|
||||
58
oauth2-resource-server/scripts/rotation-demo.sh
Executable file
58
oauth2-resource-server/scripts/rotation-demo.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Key rotation as three separate events, with the resource server watched in between.
|
||||
# Requires the stub issuer and a resource server pointed at it.
|
||||
set -eu
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
PROFILES="${1:-unknown}"
|
||||
|
||||
probe() { # prints just the status code for a token
|
||||
local token="$1"
|
||||
curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $token" "$RS/api/me"
|
||||
}
|
||||
|
||||
head1 "resource server profiles: $PROFILES"
|
||||
curl -s -X POST "$STUB/admin/reset-counter" >/dev/null
|
||||
echo "Issuer state at the start:"; stub_state
|
||||
|
||||
head1 "0. Warm the cache"
|
||||
OLD=$(stub_token "sub=alice&aud=reports-api")
|
||||
echo "token signed with $(curl -s "$STUB/admin/state" | python3 -c 'import json,sys;print(json.load(sys.stdin)["activeKid"])')"
|
||||
echo "GET /api/me -> $(probe "$OLD")"
|
||||
echo "jwks fetches so far: $(stub_fetches)"
|
||||
|
||||
head1 "1. PUBLISH a second key. Nothing signs with it yet."
|
||||
NEW=$(curl -s -X POST "$STUB/admin/publish" | python3 -c 'import json,sys;print(json.load(sys.stdin)["publishedKids"][-1])')
|
||||
echo "published: $NEW"
|
||||
stub_state
|
||||
echo
|
||||
echo "The resource server has not been told. Its cached JWK Set still holds one key."
|
||||
echo "Old token still works: $(probe "$OLD")"
|
||||
echo "jwks fetches so far: $(stub_fetches) <- unchanged: nothing forced a refresh"
|
||||
|
||||
head1 "2. ACTIVATE the new key. The issuer starts signing with it."
|
||||
curl -s -X POST "$STUB/admin/activate?kid=$NEW" >/dev/null
|
||||
NEWTOK=$(stub_token "sub=alice&aud=reports-api")
|
||||
echo "A token arrives whose kid is not in the cached JWK Set."
|
||||
echo "New token: $(probe "$NEWTOK")"
|
||||
echo "jwks fetches so far: $(stub_fetches) <- the unknown kid forced one"
|
||||
echo
|
||||
echo "This is the recovery path, and it works. It is also the only thing in the default"
|
||||
echo "configuration that notices a rotation, because refresh-ahead is switched off."
|
||||
|
||||
head1 "3. Tokens signed with the old key are still in flight"
|
||||
echo "They were minted before the switch and have not expired yet."
|
||||
echo "Old token: $(probe "$OLD") <- still accepted, because the old key is still published"
|
||||
|
||||
head1 "4. RETIRE the old key from the JWK Set"
|
||||
OLDKID=$(curl -s "$STUB/admin/state" | python3 -c 'import json,sys;print(json.load(sys.stdin)["publishedKids"][0])')
|
||||
curl -s -X POST "$STUB/admin/retire?kid=$OLDKID" >/dev/null
|
||||
echo "retired: $OLDKID"
|
||||
stub_state
|
||||
echo
|
||||
echo "The resource server's cache still contains it, so nothing changes yet."
|
||||
echo "Old token: $(probe "$OLD")"
|
||||
echo "New token: $(probe "$NEWTOK")"
|
||||
echo "jwks fetches so far: $(stub_fetches)"
|
||||
echo
|
||||
echo "How long the old key keeps working from here is decided entirely by the cache."
|
||||
echo "See retired-key-demo.sh, which runs this same step under two cache configurations."
|
||||
100
oauth2-resource-server/scripts/run-all.sh
Executable file
100
oauth2-resource-server/scripts/run-all.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/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."
|
||||
16
oauth2-resource-server/scripts/run-rs.sh
Executable file
16
oauth2-resource-server/scripts/run-rs.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Starts the resource server on :8081 with the given profiles, replacing any previous instance.
|
||||
# Usage: ./scripts/run-rs.sh stub,roles,audience
|
||||
set -eu
|
||||
PROFILES="${1:-stub}"
|
||||
cd "$(dirname "$0")/.."
|
||||
for p in $(ps -eo pid,cmd | grep '[R]esourceServerApplication' | awk '{print $1}'); do kill -9 "$p" || true; done
|
||||
sleep 1
|
||||
setsid nohup mvn -B -o org.springframework.boot:spring-boot-maven-plugin:run \
|
||||
-Dspring-boot.run.main-class=com.ankurm.rsdemo.ResourceServerApplication \
|
||||
-Dspring-boot.run.profiles="$PROFILES" > "/tmp/rs-${PROFILES//,/-}.log" 2>&1 < /dev/null &
|
||||
for i in $(seq 1 60); do
|
||||
curl -sf http://localhost:8081/api/public/ping -o /dev/null 2>/dev/null && { echo "resource server up on :8081 with profiles: $PROFILES"; exit 0; }
|
||||
sleep 2
|
||||
done
|
||||
echo "resource server failed to start; see /tmp/rs-${PROFILES//,/-}.log" >&2; exit 1
|
||||
14
oauth2-resource-server/scripts/run-stub-issuer.sh
Executable file
14
oauth2-resource-server/scripts/run-stub-issuer.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Starts the stub authorization server on :9000, replacing any previous instance.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
for p in $(ps -eo pid,cmd | grep '[S]tubIssuerApplication' | awk '{print $1}'); do kill -9 "$p" || true; done
|
||||
sleep 1
|
||||
setsid nohup mvn -B -o org.springframework.boot:spring-boot-maven-plugin:run \
|
||||
-Dspring-boot.run.main-class=com.ankurm.stubissuer.StubIssuerApplication \
|
||||
-Dspring-boot.run.profiles=stubissuer > /tmp/stub-issuer.log 2>&1 < /dev/null &
|
||||
for i in $(seq 1 60); do
|
||||
curl -sf http://localhost:9000/admin/state -o /dev/null 2>/dev/null && { echo "stub issuer up on :9000"; exit 0; }
|
||||
sleep 2
|
||||
done
|
||||
echo "stub issuer failed to start; see /tmp/stub-issuer.log" >&2; exit 1
|
||||
Reference in New Issue
Block a user