1
0

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:
2026-08-23 11:00:56 +00:00
parent 4a8dab6739
commit 4dc45d5e00
101 changed files with 4087 additions and 64 deletions

View 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"

View 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"

View 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

View 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"

View 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"

View 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
}

View 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."

View 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."

View 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."

View 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

View 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