Add Spring Authorization Server project: OAuth2/OIDC provider, client and resource server
Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.
Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
in 7.0, and both configuration classes moved into spring-security-config
- ClientSettings.requireProofKey flipped from false to true, on the authorization server
(1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
- requireProofKey(false) does not make PKCE optional for a public client; the code
verifier is that client's only authentication at the token endpoint
- MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called
Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
This commit is contained in:
35
authorization-server/scripts/audience.sh
Executable file
35
authorization-server/scripts/audience.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# The gap between "the signature is valid" and "this token was meant for me".
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
OUT=../docs/output/as-audience.txt
|
||||
mkdir -p ../docs/output
|
||||
{
|
||||
section "A token for a DIFFERENT audience, signed by the SAME issuer"
|
||||
echo "demo-service's tokens carry aud=[orders-api] thanks to the token customiser."
|
||||
echo "Here we ask for one and then present it to a resource server configured to"
|
||||
echo "require a different audience - and to one that does not check at all."
|
||||
TOKEN=$(curl -s -u demo-service:service-secret -d grant_type=client_credentials \
|
||||
-d scope=orders.read "$AS/oauth2/token" \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin).get("access_token",""))')
|
||||
echo
|
||||
echo "aud claim in the token:"
|
||||
jwt_payload "$TOKEN" | grep -A3 '"aud"'
|
||||
|
||||
section "Resource server running with demo.validate-audience=false"
|
||||
echo "This is the Spring Boot default: issuer-uri alone validates signature, exp/nbf"
|
||||
echo "and iss. Audience is not checked unless you add a validator."
|
||||
curl -s -o /tmp/b -w 'GET /api/orders -> %{http_code}\n' -H "Authorization: Bearer $TOKEN" "$RS/api/orders"
|
||||
head -c 300 /tmp/b; echo
|
||||
|
||||
section "The same token with a deliberately mangled signature"
|
||||
BAD="${TOKEN%?}X"
|
||||
curl -s -D - -o /dev/null -H "Authorization: Bearer $BAD" "$RS/api/orders" \
|
||||
| sed -n '1p;/^WWW-Authenticate/p'
|
||||
|
||||
section "No token at all"
|
||||
curl -s -D - -o /dev/null "$RS/api/orders" | sed -n '1p;/^WWW-Authenticate/p'
|
||||
} > "$OUT" 2>&1
|
||||
sed -i 's/[[:space:]]*$//' "$OUT"
|
||||
echo "wrote $OUT"
|
||||
256
authorization-server/scripts/authcode-pkce.sh
Executable file
256
authorization-server/scripts/authcode-pkce.sh
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bash
|
||||
# The full authorization code flow with PKCE, driven entirely by curl so that every
|
||||
# redirect, form and parameter is visible. A browser hides all of this.
|
||||
#
|
||||
# ./scripts/authcode-pkce.sh [output-name] [client]
|
||||
#
|
||||
# client defaults to demo-spa (public, PKCE required). Pass demo-web for the confidential
|
||||
# client with a secret.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
|
||||
NAME="${1:-as-authcode-pkce}"
|
||||
CLIENT="${2:-demo-spa}"
|
||||
# NO_CHALLENGE=1 sends an authorization request with no code_challenge at all. That is a
|
||||
# different thing from sending one and then omitting the verifier: the server only demands
|
||||
# a verifier if the authorization request carried a challenge, OR if the client is
|
||||
# registered with requireProofKey(true).
|
||||
NO_CHALLENGE="${NO_CHALLENGE:-0}"
|
||||
OUT="../docs/output/${NAME}.txt"
|
||||
mkdir -p ../docs/output
|
||||
|
||||
if [ "$CLIENT" = "demo-web" ]; then
|
||||
REDIRECT="http://127.0.0.1:8080/login/oauth2/code/demo-web"
|
||||
SCOPE="openid orders.read orders.write"
|
||||
else
|
||||
REDIRECT="http://127.0.0.1:8080/authorized"
|
||||
SCOPE="openid orders.read"
|
||||
fi
|
||||
|
||||
JAR=$(mktemp)
|
||||
trap 'rm -f "$JAR" /tmp/as-page.html' EXIT
|
||||
|
||||
# --- PKCE parameters. RFC 7636: verifier is 43-128 chars of unreserved characters,
|
||||
# --- challenge is BASE64URL(SHA256(verifier)) with the padding stripped.
|
||||
read -r VERIFIER CHALLENGE <<<"$(python3 - <<'PY'
|
||||
import base64, hashlib, secrets
|
||||
v = base64.urlsafe_b64encode(secrets.token_bytes(48)).decode().rstrip('=')
|
||||
c = base64.urlsafe_b64encode(hashlib.sha256(v.encode()).digest()).decode().rstrip('=')
|
||||
print(v, c)
|
||||
PY
|
||||
)"
|
||||
|
||||
{
|
||||
section "PKCE parameters (RFC 7636)"
|
||||
echo "code_verifier ${VERIFIER} (${#VERIFIER} chars)"
|
||||
echo "code_challenge ${CHALLENGE}"
|
||||
echo "code_challenge_method S256"
|
||||
echo
|
||||
echo "The verifier never leaves the client until the token request. The challenge is"
|
||||
echo "all the authorization request carries, and it is a one-way hash of the verifier."
|
||||
|
||||
section "1. Log in to the authorization server (browser session)"
|
||||
# The login page carries a CSRF token; the form chain has CSRF enabled, as it should.
|
||||
curl -s -c "$JAR" "$AS/login" -o /tmp/as-page.html
|
||||
CSRF=$(form_value /tmp/as-page.html _csrf)
|
||||
echo "\$ curl -c jar -d username=alice -d password=password -d _csrf=<token> $AS/login"
|
||||
curl -s -i -b "$JAR" -c "$JAR" \
|
||||
-d "username=alice" -d "password=password" -d "_csrf=$CSRF" \
|
||||
"$AS/login" | sed -n '1p;/^[Ll]ocation:/p'
|
||||
|
||||
section "2. GET /oauth2/authorize (client=$CLIENT)"
|
||||
PKCE_PARAMS="&code_challenge=$CHALLENGE&code_challenge_method=S256"
|
||||
if [ "$NO_CHALLENGE" = "1" ]; then
|
||||
PKCE_PARAMS=""
|
||||
echo "NO_CHALLENGE=1: the authorization request carries no code_challenge."
|
||||
echo
|
||||
fi
|
||||
AUTHZ="$AS/oauth2/authorize?response_type=code&client_id=$CLIENT&redirect_uri=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=''))" "$REDIRECT")&scope=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$SCOPE")&state=xyz123${PKCE_PARAMS}"
|
||||
echo "\$ curl -b jar '$AUTHZ'"
|
||||
LOC=$(curl -s -i -b "$JAR" -c "$JAR" "$AUTHZ" | tr -d '\r' | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
echo "-> 302 $LOC"
|
||||
|
||||
if [ -z "$LOC" ]; then
|
||||
echo "no redirect - the authorization endpoint rendered a page instead:"
|
||||
curl -s -b "$JAR" "$AUTHZ" | head -30
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$LOC" in
|
||||
*/oauth2/consent*)
|
||||
section "3. The consent page"
|
||||
echo "The authorization endpoint redirected to OUR page, at the path given to"
|
||||
echo ".consentPage(\"/oauth2/consent\"). Note the query string it hands over:"
|
||||
echo "$LOC" | tr '&' '\n' | sed 's/^/ /'
|
||||
curl -s -b "$JAR" -c "$JAR" "$AS${LOC#*9000}" -o /tmp/as-page.html
|
||||
echo
|
||||
echo "Scopes rendered as checkboxes (openid deliberately not among them):"
|
||||
form_values /tmp/as-page.html scope | sed 's/^/ /'
|
||||
CSRF=$(form_value /tmp/as-page.html _csrf)
|
||||
STATE=$(form_value /tmp/as-page.html state)
|
||||
echo
|
||||
echo "The hidden state the form must echo back: $STATE"
|
||||
echo "(this is NOT the client's state=xyz123 - it is the server's own correlation"
|
||||
echo " handle for the pending authorization request, and sending the client's value"
|
||||
echo " instead is what produces the consent redirect loop)"
|
||||
|
||||
section "4. POST the approval to /oauth2/authorize"
|
||||
ARGS=(-d "client_id=$CLIENT" -d "state=$STATE" -d "_csrf=$CSRF")
|
||||
for s in $(form_values /tmp/as-page.html scope); do
|
||||
ARGS+=(-d "scope=$s")
|
||||
done
|
||||
echo "\$ curl -b jar -X POST ${ARGS[*]} $AS/oauth2/authorize"
|
||||
LOC=$(curl -s -i -b "$JAR" -c "$JAR" "${ARGS[@]}" "$AS/oauth2/authorize" \
|
||||
| tr -d '\r' | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
echo "-> 302 $LOC"
|
||||
;;
|
||||
*)
|
||||
section "3. No consent page"
|
||||
echo "The authorization endpoint went straight back to the client. Either consent is"
|
||||
echo "off for this client, or every requested scope was already approved."
|
||||
;;
|
||||
esac
|
||||
|
||||
CODE=$(echo "$LOC" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p')
|
||||
RETURNED_STATE=$(echo "$LOC" | sed -n 's/.*[?&]state=\([^&]*\).*/\1/p')
|
||||
section "5. The authorization code"
|
||||
echo "code = $CODE"
|
||||
echo "state = $RETURNED_STATE (the client's own value, returned untouched - compare it)"
|
||||
if [ -z "$CODE" ]; then
|
||||
echo "no code in the redirect. The error was:"
|
||||
echo "$LOC" | tr '&' '\n' | sed 's/^/ /'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
section "6a. Exchange the code WITHOUT the verifier"
|
||||
echo "This is the request an attacker who stole the code can make."
|
||||
AUTH_ARGS=()
|
||||
[ "$CLIENT" = "demo-web" ] && AUTH_ARGS=(-u demo-web:web-secret)
|
||||
NOVERIFIER=$(curl -s -w '\n<<HTTP %{http_code}>>' "${AUTH_ARGS[@]}" \
|
||||
-d grant_type=authorization_code -d "code=$CODE" \
|
||||
-d "redirect_uri=$REDIRECT" -d "client_id=$CLIENT" \
|
||||
"$AS/oauth2/token")
|
||||
echo "$NOVERIFIER" | sed -n 's/^<<HTTP \(.*\)>>$/HTTP \1/p'
|
||||
BODY=${NOVERIFIER%%$'\n'<<HTTP*}
|
||||
if [ -n "$BODY" ]; then
|
||||
echo "$BODY" | python3 -m json.tool 2>/dev/null || echo "$BODY"
|
||||
else
|
||||
echo "(empty response body)"
|
||||
fi
|
||||
echo
|
||||
case "$NOVERIFIER" in
|
||||
*access_token*)
|
||||
echo ">>> A TOKEN WAS ISSUED. The code alone was sufficient. This is what"
|
||||
echo ">>> requireProofKey(false) on a public client means in practice."
|
||||
;;
|
||||
*)
|
||||
echo ">>> Rejected. invalid_grant is deliberately vague: the server will not tell"
|
||||
echo ">>> a caller whether the code was wrong, expired, already used, or missing a"
|
||||
echo ">>> verifier, because each of those is information an attacker can use."
|
||||
;;
|
||||
esac
|
||||
echo "Note: this consumed the code. Authorization codes are single-use, so the"
|
||||
echo "successful exchange below needs a fresh one."
|
||||
|
||||
section "6b. A fresh code, exchanged properly"
|
||||
LOC=$(curl -s -i -b "$JAR" -c "$JAR" "$AUTHZ" | tr -d '\r' | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
case "$LOC" in
|
||||
*/oauth2/consent*)
|
||||
curl -s -b "$JAR" -c "$JAR" "$AS${LOC#*9000}" -o /tmp/as-page.html
|
||||
CSRF=$(form_value /tmp/as-page.html _csrf)
|
||||
STATE=$(form_value /tmp/as-page.html state)
|
||||
ARGS=(-d "client_id=$CLIENT" -d "state=$STATE" -d "_csrf=$CSRF")
|
||||
for s in $(form_values /tmp/as-page.html scope); do
|
||||
ARGS+=(-d "scope=$s")
|
||||
done
|
||||
LOC=$(curl -s -i -b "$JAR" -c "$JAR" "${ARGS[@]}" "$AS/oauth2/authorize" \
|
||||
| tr -d '\r' | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
;;
|
||||
esac
|
||||
CODE=$(echo "$LOC" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p')
|
||||
echo "fresh code = $CODE"
|
||||
echo
|
||||
if [ "$NO_CHALLENGE" = "1" ]; then
|
||||
echo "\$ curl -d grant_type=authorization_code -d code=... $AS/oauth2/token"
|
||||
echo " (no code_verifier - there was no challenge to verify against)"
|
||||
else
|
||||
echo "\$ curl -d grant_type=authorization_code -d code=... -d code_verifier=... $AS/oauth2/token"
|
||||
fi
|
||||
VERIFIER_ARG=(-d "code_verifier=$VERIFIER")
|
||||
[ "$NO_CHALLENGE" = "1" ] && VERIFIER_ARG=()
|
||||
HTTPCODE=$(curl -s -o /tmp/as-tok.json -w '%{http_code}' "${AUTH_ARGS[@]}" \
|
||||
-d grant_type=authorization_code -d "code=$CODE" \
|
||||
-d "redirect_uri=$REDIRECT" -d "client_id=$CLIENT" \
|
||||
"${VERIFIER_ARG[@]}" \
|
||||
"$AS/oauth2/token")
|
||||
RESP=$(cat /tmp/as-tok.json)
|
||||
echo "HTTP $HTTPCODE"
|
||||
if [ -s /tmp/as-tok.json ]; then
|
||||
python3 -m json.tool < /tmp/as-tok.json 2>/dev/null || cat /tmp/as-tok.json
|
||||
else
|
||||
echo "(empty response body)"
|
||||
fi
|
||||
if [ "$HTTPCODE" != "200" ] && [ "$NO_CHALLENGE" = "1" ]; then
|
||||
echo
|
||||
echo ">>> No token, even though the client is registered with requireProofKey(false)"
|
||||
echo ">>> and the authorization request carried no challenge. The reason is that a"
|
||||
echo ">>> public client has no other way to authenticate at the token endpoint:"
|
||||
echo ">>> PublicClientAuthenticationProvider delegates entirely to"
|
||||
echo ">>> CodeVerifierAuthenticator, and raises invalid_client when there is nothing"
|
||||
echo ">>> to verify. requireProofKey(false) relaxes the AUTHORIZATION endpoint only."
|
||||
fi
|
||||
|
||||
read_claim() { python3 -c 'import sys,json
|
||||
try: print(json.load(sys.stdin).get(sys.argv[1],""))
|
||||
except Exception: print("")' "$1" < /tmp/as-tok.json; }
|
||||
AT=$(read_claim access_token)
|
||||
IDT=$(read_claim id_token)
|
||||
RT=$(read_claim refresh_token)
|
||||
|
||||
if [ -n "$AT" ]; then
|
||||
section "7. The access token"
|
||||
jwt_header "$AT"
|
||||
jwt_payload "$AT"
|
||||
fi
|
||||
if [ -n "$IDT" ]; then
|
||||
section "8. The id_token - a different token, for a different audience"
|
||||
jwt_payload "$IDT"
|
||||
echo
|
||||
echo "aud is the CLIENT here, not the API. Sending this to a resource server is the"
|
||||
echo "classic mix-up: it verifies (same issuer, same key) and then fails the audience"
|
||||
echo "check, or worse, passes it if nobody checks audience."
|
||||
fi
|
||||
|
||||
if [ -n "$AT" ]; then
|
||||
section "9. Calling the resource server"
|
||||
for path in /api/orders /api/admin; do
|
||||
CODE_HTTP=$(curl -s -o /tmp/rsbody -w '%{http_code}' -H "Authorization: Bearer $AT" "$RS$path")
|
||||
echo "GET $path -> $CODE_HTTP"
|
||||
head -c 500 /tmp/rsbody; echo
|
||||
done
|
||||
|
||||
section "10. Sending the id_token instead"
|
||||
if [ -n "$IDT" ]; then
|
||||
curl -s -i -H "Authorization: Bearer $IDT" "$RS/api/orders" \
|
||||
| sed -n '1p;/^WWW-Authenticate/p'
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$RT" ]; then
|
||||
section "11. Refresh, with rotation"
|
||||
echo "old refresh token: ${RT:0:24}..."
|
||||
R2=$(curl -s "${AUTH_ARGS[@]}" -d grant_type=refresh_token -d "refresh_token=$RT" \
|
||||
-d "client_id=$CLIENT" "$AS/oauth2/token")
|
||||
NEW=$(echo "$R2" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("refresh_token",""))')
|
||||
echo "new refresh token: ${NEW:0:24}..."
|
||||
[ "$RT" = "$NEW" ] && echo "SAME - reuseRefreshTokens(true)" || echo "DIFFERENT - reuseRefreshTokens(false), the old one is now dead"
|
||||
echo
|
||||
echo "Replaying the old one:"
|
||||
curl -s "${AUTH_ARGS[@]}" -d grant_type=refresh_token -d "refresh_token=$RT" \
|
||||
-d "client_id=$CLIENT" "$AS/oauth2/token"
|
||||
echo
|
||||
fi
|
||||
} > "$OUT" 2>&1
|
||||
sed -i 's/[[:space:]]*$//' "$OUT"
|
||||
echo "wrote $OUT"; tail -5 "$OUT"
|
||||
63
authorization-server/scripts/client-credentials.sh
Executable file
63
authorization-server/scripts/client-credentials.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# The simplest grant, and what the token customiser does and does not add to it.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
OUT="../docs/output/${1:-as-client-credentials}.txt"
|
||||
mkdir -p ../docs/output
|
||||
{
|
||||
section "POST /oauth2/token grant_type=client_credentials"
|
||||
echo "\$ curl -su demo-service:service-secret -d grant_type=client_credentials \\"
|
||||
echo " -d scope=orders.read $AS/oauth2/token"
|
||||
RESP=$(curl -s -u demo-service:service-secret \
|
||||
-d grant_type=client_credentials -d scope=orders.read \
|
||||
"$AS/oauth2/token")
|
||||
echo "$RESP" | python3 -m json.tool
|
||||
|
||||
TOKEN=$(echo "$RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("access_token",""))')
|
||||
if [ -z "$TOKEN" ]; then echo "no access token - stopping"; exit 1; fi
|
||||
|
||||
case "$TOKEN" in
|
||||
*.*.*)
|
||||
section "JOSE header"
|
||||
jwt_header "$TOKEN"
|
||||
section "Claims"
|
||||
jwt_payload "$TOKEN"
|
||||
;;
|
||||
*)
|
||||
section "Not a JWT"
|
||||
echo "The access token is an opaque reference: $TOKEN"
|
||||
echo "Length ${#TOKEN}. It carries no claims; the resource server must introspect it."
|
||||
section "POST /oauth2/introspect"
|
||||
curl -s -u demo-service:service-secret -d "token=$TOKEN" \
|
||||
"$AS/oauth2/introspect" | python3 -m json.tool
|
||||
;;
|
||||
esac
|
||||
|
||||
section "Wrong secret"
|
||||
echo "\$ curl -si -u demo-service:WRONG -d grant_type=client_credentials $AS/oauth2/token"
|
||||
curl -s -i -u demo-service:WRONG -d grant_type=client_credentials \
|
||||
"$AS/oauth2/token" | sed -n '1p;/^WWW-Authenticate/p;/^{/p'
|
||||
|
||||
section "A grant the client is not registered for"
|
||||
echo "\$ curl -si -u demo-service:service-secret -d grant_type=authorization_code -d code=x $AS/oauth2/token"
|
||||
curl -s -i -u demo-service:service-secret -d grant_type=authorization_code -d code=x \
|
||||
"$AS/oauth2/token" | sed -n '1p;/^{/p'
|
||||
|
||||
section "A scope the client is not registered for"
|
||||
echo "\$ curl -s -u demo-service:service-secret -d grant_type=client_credentials -d scope=orders.write $AS/oauth2/token"
|
||||
curl -s -u demo-service:service-secret -d grant_type=client_credentials -d scope=orders.write \
|
||||
"$AS/oauth2/token"
|
||||
echo
|
||||
|
||||
section "Calling the resource server with the token"
|
||||
for path in /public /api/orders /api/admin; do
|
||||
CODE=$(curl -s -o /tmp/rsbody -w '%{http_code}' -H "Authorization: Bearer $TOKEN" "$RS$path")
|
||||
WWW=$(curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" "$RS$path" | grep -i '^WWW-Authenticate' || true)
|
||||
echo "GET $path -> $CODE"
|
||||
[ -n "$WWW" ] && echo " $WWW"
|
||||
head -c 400 /tmp/rsbody; echo
|
||||
done
|
||||
} > "$OUT" 2>&1
|
||||
sed -i 's/[[:space:]]*$//' "$OUT"
|
||||
echo "wrote $OUT"
|
||||
110
authorization-server/scripts/client-flow.sh
Executable file
110
authorization-server/scripts/client-flow.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drives the real Spring OAuth2 client through a real browser flow with curl, so the
|
||||
# behaviour is the client's own and not this script's.
|
||||
#
|
||||
# ./scripts/client-flow.sh <output-name> <label>
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
CLIENT=http://127.0.0.1:8080
|
||||
LAST_AUTHZ=""
|
||||
NAME="${1:-as-client-flow}"
|
||||
LABEL="${2:-default}"
|
||||
OUT="../docs/output/${NAME}.txt"
|
||||
mkdir -p ../docs/output
|
||||
JAR=$(mktemp); trap 'rm -f "$JAR" /tmp/cf.html' EXIT
|
||||
|
||||
follow() { # url -> prints status + location, follows same-host redirects up to 6 hops
|
||||
local url="$1" hop=0
|
||||
while [ $hop -lt 8 ]; do
|
||||
local hdrs status loc
|
||||
# A browser sends Accept: text/html. curl's default is */*, and with the entry-point
|
||||
# matcher configured to ignore */* that difference decides whether /oauth2/authorize
|
||||
# redirects you to the login page or answers 401.
|
||||
hdrs=$(curl -s -D - -o /tmp/cf.html -b "$JAR" -c "$JAR" -H 'Accept: text/html' "$url" | tr -d '\r')
|
||||
status=$(echo "$hdrs" | head -1 | awk '{print $2}')
|
||||
loc=$(echo "$hdrs" | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
echo " $status $url"
|
||||
case "$url" in *"/oauth2/authorize?"*) LAST_AUTHZ="$url" ;; esac
|
||||
[ -z "$loc" ] && { LAST_URL="$url"; return 0; }
|
||||
case "$loc" in http*) url="$loc" ;; *) url="$(echo "$url" | grep -oE '^https?://[^/]+')$loc" ;; esac
|
||||
hop=$((hop+1))
|
||||
done
|
||||
LAST_URL="$url"
|
||||
}
|
||||
|
||||
{
|
||||
section "The relying party drives the flow [$LABEL]"
|
||||
echo "GET $CLIENT/orders while unauthenticated. Every hop below is a real redirect."
|
||||
echo
|
||||
follow "$CLIENT/orders"
|
||||
echo
|
||||
echo "The authorization request the client built:"
|
||||
echo "$LAST_AUTHZ" | tr '&?' '\n\n' | sed 's/^/ /'
|
||||
case "$LAST_AUTHZ" in
|
||||
*code_challenge*) echo " >>> code_challenge IS present" ;;
|
||||
*) echo " >>> NO code_challenge - a client registered with"
|
||||
echo " >>> requireProofKey(true) will reject this outright" ;;
|
||||
esac
|
||||
|
||||
case "$LAST_URL" in
|
||||
"$AS/login"*)
|
||||
echo
|
||||
echo "Landed on the authorization server's login page. Submitting credentials:"
|
||||
CSRF=$(form_value /tmp/cf.html _csrf)
|
||||
HDRS=$(curl -s -D - -o /dev/null -b "$JAR" -c "$JAR" -H 'Accept: text/html' \
|
||||
-d username=alice -d password=password -d "_csrf=$CSRF" \
|
||||
"$AS/login" | tr -d '\r')
|
||||
echo " $(echo "$HDRS" | head -1 | awk '{print $2}') POST $AS/login"
|
||||
NEXT=$(echo "$HDRS" | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
echo
|
||||
echo "Resuming the authorization request:"
|
||||
follow "$NEXT"
|
||||
;;
|
||||
esac
|
||||
|
||||
# The consent page, if we reached it.
|
||||
if grep -q 'name="scope"' /tmp/cf.html 2>/dev/null; then
|
||||
echo
|
||||
echo "Consent page reached. Approving:"
|
||||
CSRF=$(form_value /tmp/cf.html _csrf)
|
||||
STATE=$(form_value /tmp/cf.html state)
|
||||
CID=$(form_value /tmp/cf.html client_id)
|
||||
ARGS=(-d "client_id=$CID" -d "state=$STATE" -d "_csrf=$CSRF")
|
||||
for s in $(form_values /tmp/cf.html scope); do ARGS+=(-d "scope=$s"); done
|
||||
HDRS=$(curl -s -D - -o /dev/null -b "$JAR" -c "$JAR" -H 'Accept: text/html' "${ARGS[@]}" "$AS/oauth2/authorize" | tr -d '\r')
|
||||
echo " $(echo "$HDRS" | head -1 | awk '{print $2}') POST $AS/oauth2/authorize"
|
||||
NEXT=$(echo "$HDRS" | grep -i '^location:' | head -1 | sed 's/^[Ll]ocation: *//')
|
||||
echo
|
||||
echo "Back to the client with the code:"
|
||||
follow "$NEXT"
|
||||
fi
|
||||
|
||||
case "$LAST_URL" in
|
||||
*error*)
|
||||
echo
|
||||
echo "The flow ended at the CLIENT's error page, not the provider's. The provider"
|
||||
echo "rejected the authorization request and redirected the failure back to the"
|
||||
echo "registered redirect_uri, so nothing in the client's logs names the provider"
|
||||
echo "as the cause. The reason is only in the query string above."
|
||||
;;
|
||||
esac
|
||||
|
||||
section "What the client rendered"
|
||||
if grep -qi 'error' /tmp/cf.html && ! grep -q 'orders' /tmp/cf.html; then
|
||||
echo "An error page. The provider rejected the authorization request:"
|
||||
echo "$LAST_URL" | tr '&?' '\n\n' | sed 's/^/ /'
|
||||
echo
|
||||
python3 -c "
|
||||
import html,re,sys
|
||||
t = re.sub(r'<[^>]+>', ' ', open('/tmp/cf.html', encoding='utf-8', errors='replace').read())
|
||||
print(' '.join(html.unescape(t).split())[:600])"
|
||||
else
|
||||
python3 -c "
|
||||
import html,re
|
||||
t = re.sub(r'<[^>]+>', '\n', open('/tmp/cf.html', encoding='utf-8', errors='replace').read())
|
||||
print('\n'.join(l.strip() for l in html.unescape(t).splitlines() if l.strip())[:1400])"
|
||||
fi
|
||||
} > "$OUT" 2>&1
|
||||
sed -i 's/[[:space:]]*$//' "$OUT"
|
||||
echo "wrote $OUT"
|
||||
34
authorization-server/scripts/compile-legacy.sh
Executable file
34
authorization-server/scripts/compile-legacy.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles the SAS 1.x-style configuration against the 7.1.1 classpath and records the
|
||||
# compiler's own words. The point is that the error text is what you will actually see,
|
||||
# not a paraphrase of it.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=../docs/output/as-legacy-compile-failure.txt
|
||||
mkdir -p ../docs/output
|
||||
|
||||
mvn -B -q -pl auth-server dependency:build-classpath \
|
||||
-Dmdep.outputFile=/tmp/as-cp.txt -DincludeScope=compile >/dev/null 2>&1
|
||||
CP=$(cat /tmp/as-cp.txt)
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
mkdir -p "$WORK/com/ankurm/authserver/legacy" "$WORK/out"
|
||||
cp src-broken/LegacySasConfig.java.txt "$WORK/com/ankurm/authserver/legacy/LegacySasConfig.java"
|
||||
|
||||
javac -nowarn -d "$WORK/out" -cp "$CP" \
|
||||
"$WORK/com/ankurm/authserver/legacy/LegacySasConfig.java" > "$WORK/err.txt" 2>&1
|
||||
STATUS=$?
|
||||
|
||||
{
|
||||
echo "# The SAS 1.x configuration, compiled against Spring Boot 4.1.1 / Spring Security 7.1.1."
|
||||
echo "# Source: src-broken/LegacySasConfig.java.txt"
|
||||
echo
|
||||
echo "\$ javac -cp <spring-boot-4.1.1 classpath> LegacySasConfig.java"
|
||||
echo
|
||||
sed "s|$WORK|.|g" "$WORK/err.txt"
|
||||
echo
|
||||
echo "javac exit status: $STATUS"
|
||||
} > "$OUT"
|
||||
|
||||
rm -rf "$WORK"
|
||||
cat "$OUT"
|
||||
30
authorization-server/scripts/discovery.sh
Executable file
30
authorization-server/scripts/discovery.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# What the provider advertises, and the difference between the two metadata documents.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
OUT=../docs/output/as-discovery.txt
|
||||
mkdir -p ../docs/output
|
||||
{
|
||||
section "OpenID Connect discovery: GET /.well-known/openid-configuration"
|
||||
echo "\$ curl -s $AS/.well-known/openid-configuration"
|
||||
curl -s "$AS/.well-known/openid-configuration" | python3 -m json.tool
|
||||
|
||||
section "OAuth2 metadata: GET /.well-known/oauth-authorization-server"
|
||||
echo "Present even with .oidc(...) switched off. The OIDC document above is the one"
|
||||
echo "that additionally advertises userinfo_endpoint and id_token signing algorithms."
|
||||
echo "\$ curl -s $AS/.well-known/oauth-authorization-server"
|
||||
curl -s "$AS/.well-known/oauth-authorization-server" | python3 -m json.tool
|
||||
|
||||
section "JWK Set: GET /oauth2/jwks"
|
||||
echo "Public keys only. No 'd' member - if you ever see one here, stop the server."
|
||||
curl -s "$AS/oauth2/jwks" | python3 -m json.tool
|
||||
|
||||
section "Resolved endpoint settings, read back from AuthorizationServerSettings"
|
||||
curl -s "$AS/diag/settings" | python3 -m json.tool
|
||||
|
||||
section "Registered clients, as the server actually holds them"
|
||||
curl -s "$AS/diag/clients" | python3 -m json.tool
|
||||
} > "$OUT" 2>&1
|
||||
sed -i 's/[[:space:]]*$//' "$OUT"
|
||||
echo "wrote $OUT"; wc -l "$OUT"
|
||||
37
authorization-server/scripts/entrypoint-accept.sh
Executable file
37
authorization-server/scripts/entrypoint-accept.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Why a failed token request sometimes answers 302 -> /login instead of 401.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
OUT=../docs/output/as-entrypoint-accept.txt
|
||||
LABEL="${1:-}"
|
||||
mkdir -p ../docs/output
|
||||
probe() {
|
||||
local accept="$1" desc="$2"
|
||||
echo "--- $desc"
|
||||
echo "\$ curl -H 'Accept: $accept' -d grant_type=authorization_code -d code=bogus \\"
|
||||
echo " -d client_id=demo-spa $AS/oauth2/token"
|
||||
curl -s -i -H "Accept: $accept" -d grant_type=authorization_code -d code=bogus \
|
||||
-d client_id=demo-spa "$AS/oauth2/token" \
|
||||
| sed -n '1p;/^[Ll]ocation:/p;/^WWW-Authenticate/p'
|
||||
echo
|
||||
}
|
||||
{
|
||||
section "Public client, failed authentication at the token endpoint [$LABEL]"
|
||||
echo "A public client authenticates at /oauth2/token by presenting a code_verifier."
|
||||
echo "With no verifier there is nothing to authenticate with, so the request falls"
|
||||
echo "through to the AuthenticationEntryPoint - and which entry point runs depends on"
|
||||
echo "the Accept header."
|
||||
echo
|
||||
probe "*/*" "Accept: */* (curl's default, and most HTTP clients')"
|
||||
probe "application/json" "Accept: application/json"
|
||||
probe "text/html" "Accept: text/html (a browser)"
|
||||
|
||||
section "Confidential client with a wrong secret, for contrast"
|
||||
echo "This never reaches the entry point: OAuth2ClientAuthenticationFilter writes the"
|
||||
echo "error itself, so the Accept header makes no difference."
|
||||
curl -s -i -u demo-web:wrong -d grant_type=client_credentials "$AS/oauth2/token" \
|
||||
| sed -n '1p;/^[Ll]ocation:/p'
|
||||
} >> "$OUT" 2>&1
|
||||
sed -i 's/[[:space:]]*$//' "$OUT"
|
||||
echo "appended $LABEL to $OUT"
|
||||
91
authorization-server/scripts/lib.sh
Executable file
91
authorization-server/scripts/lib.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers. Sourced by every demo script.
|
||||
AS=http://localhost:9000
|
||||
RS=http://localhost:8090
|
||||
|
||||
# Kill by main class, never by a pattern that could match this script's own command line.
|
||||
# `pkill -f spring-boot` matches the shell running it and takes the shell with it.
|
||||
kill_app() {
|
||||
local mainclass="$1" port="${2:-}"
|
||||
for p in $(ps -eo pid,cmd | grep "[${mainclass:0:1}]${mainclass:1}" | awk '{print $1}'); do
|
||||
kill -9 "$p" 2>/dev/null || true
|
||||
done
|
||||
# Then wait for the port to actually close. `ss -lptn` often reports the socket with no
|
||||
# PID, so a port-based kill can silently do nothing while the old process keeps serving -
|
||||
# which looks exactly like your config change having had no effect. Waiting for the
|
||||
# listener to disappear is the only reliable signal that the restart is real.
|
||||
if [ -n "$port" ]; then
|
||||
for _ in $(seq 1 30); do
|
||||
curl -s -o /dev/null --max-time 1 "http://localhost:$port/" || return 0
|
||||
sleep 1
|
||||
done
|
||||
echo "WARNING: something is still listening on :$port after kill_app $mainclass" >&2
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
}
|
||||
|
||||
wait_for() {
|
||||
local url="$1" tries="${2:-90}"
|
||||
for _ in $(seq 1 "$tries"); do
|
||||
if curl -s -o /dev/null --max-time 2 "$url"; then return 0; fi
|
||||
sleep 1
|
||||
done
|
||||
echo "timed out waiting for $url - check the app log" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
hr() { printf '%s\n' "------------------------------------------------------------------"; }
|
||||
|
||||
section() { echo; hr; echo "== $*"; hr; }
|
||||
|
||||
# Decode a JWS payload without verifying it. Debug only - never do this to decide anything.
|
||||
jwt_payload() {
|
||||
python3 - "$1" <<'PY'
|
||||
import base64, json, sys
|
||||
part = sys.argv[1].split('.')[1]
|
||||
part += '=' * (-len(part) % 4)
|
||||
print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2, sort_keys=True))
|
||||
PY
|
||||
}
|
||||
|
||||
jwt_header() {
|
||||
python3 - "$1" <<'PY'
|
||||
import base64, json, sys
|
||||
part = sys.argv[1].split('.')[0]
|
||||
part += '=' * (-len(part) % 4)
|
||||
print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2, sort_keys=True))
|
||||
PY
|
||||
}
|
||||
|
||||
# Pull a hidden input's value out of a page. Attribute order is not fixed - Spring
|
||||
# Security's default login page renders name before value, Thymeleaf renders value before
|
||||
# name - so a naive grep for name="x" value="y" works on one and silently returns empty on
|
||||
# the other. Empty CSRF token, HTTP 403, and an hour lost.
|
||||
form_value() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import re, sys
|
||||
html = open(sys.argv[1], encoding='utf-8', errors='replace').read()
|
||||
want = sys.argv[2]
|
||||
for tag in re.findall(r'<input\b[^>]*>', html, re.I):
|
||||
attrs = dict((m.group(1).lower(), m.group(2))
|
||||
for m in re.finditer(r'([\w:-]+)\s*=\s*"([^"]*)"', tag))
|
||||
if attrs.get('name') == want:
|
||||
print(attrs.get('value', ''))
|
||||
break
|
||||
PY
|
||||
}
|
||||
|
||||
# Every value of a repeated input (the scope checkboxes on the consent page).
|
||||
form_values() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import re, sys
|
||||
html = open(sys.argv[1], encoding='utf-8', errors='replace').read()
|
||||
want = sys.argv[2]
|
||||
for tag in re.findall(r'<input\b[^>]*>', html, re.I):
|
||||
attrs = dict((m.group(1).lower(), m.group(2))
|
||||
for m in re.finditer(r'([\w:-]+)\s*=\s*"([^"]*)"', tag))
|
||||
if attrs.get('name') == want and 'disabled' not in tag.lower():
|
||||
print(attrs.get('value', ''))
|
||||
PY
|
||||
}
|
||||
32
authorization-server/scripts/pkce-applier.sh
Executable file
32
authorization-server/scripts/pkce-applier.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Why a confidential Spring OAuth2 client does not send PKCE, straight from the bytecode
|
||||
# of DefaultOAuth2AuthorizationRequestResolver rather than from documentation.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=../docs/output/as-pkce-applier.txt
|
||||
mkdir -p ../docs/output
|
||||
mvn -B -q -pl oidc-client dependency:build-classpath \
|
||||
-Dmdep.outputFile=/tmp/cl-cp.txt -DincludeScope=compile >/dev/null 2>&1
|
||||
JAR=$(tr ':' '\n' < /tmp/cl-cp.txt | grep 'spring-security-oauth2-client-' | head -1)
|
||||
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
|
||||
(cd "$WORK" && unzip -o -q "$JAR" 'org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver*')
|
||||
{
|
||||
echo "# From $(basename "$JAR")"
|
||||
echo "#"
|
||||
echo "# The resolver applies its default PKCE customizer only when the registration's"
|
||||
echo "# client authentication method is NONE - that is, only for public clients."
|
||||
echo "# A registration that has a client secret gets no code_challenge."
|
||||
echo
|
||||
javap -p -c -cp "$WORK" \
|
||||
org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver \
|
||||
2>/dev/null \
|
||||
| grep -E 'ClientAuthenticationMethod.NONE|DEFAULT_PKCE_APPLIER|withPkce' \
|
||||
| sed 's/^ *//' | head -8
|
||||
echo
|
||||
echo "# The fields and the opt-in setter:"
|
||||
javap -p -cp "$WORK" \
|
||||
org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver \
|
||||
2>/dev/null \
|
||||
| grep -E 'DEFAULT_PKCE_APPLIER|setAuthorizationRequestCustomizer' | sed 's/^ *//'
|
||||
} > "$OUT" 2>&1
|
||||
cat "$OUT"
|
||||
22
authorization-server/scripts/rs-startup-failure.sh
Executable file
22
authorization-server/scripts/rs-startup-failure.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# What happens when the resource server starts and the issuer is not reachable.
|
||||
# Worth capturing because the failure is at STARTUP, not at first request - which means
|
||||
# a provider outage during a rolling deploy takes your API down with it.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
OUT=../docs/output/as-rs-startup-failure.txt
|
||||
mkdir -p ../docs/output
|
||||
kill_app ResourceServerApplication 8090
|
||||
kill_app AuthServerApplication 9000
|
||||
sleep 1
|
||||
mvn -B -o -pl resource-server org.springframework.boot:spring-boot-maven-plugin:run \
|
||||
> /tmp/rs-fail.log 2>&1 || true
|
||||
{
|
||||
echo "# resource-server started with spring.security.oauth2.resourceserver.jwt.issuer-uri"
|
||||
echo "# pointing at an authorization server that is not running."
|
||||
echo
|
||||
grep -E '^Caused by|Unable to resolve the Configuration' /tmp/rs-fail.log \
|
||||
| sed 's/^Caused by: //' | head -8
|
||||
} > "$OUT"
|
||||
cat "$OUT"
|
||||
114
authorization-server/scripts/run-all.sh
Executable file
114
authorization-server/scripts/run-all.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file in ../docs/output that belongs to this project.
|
||||
#
|
||||
# ./scripts/run-all.sh
|
||||
#
|
||||
# Starts and stops the servers itself. Takes a few minutes. The only non-deterministic
|
||||
# content is timestamps, key ids and token values, which change on every run by design.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
|
||||
start_auth() {
|
||||
kill_app AuthServerApplication 9000
|
||||
local profiles="${1:-}"
|
||||
local args=(-B -o -pl auth-server org.springframework.boot:spring-boot-maven-plugin:run)
|
||||
[ -n "$profiles" ] && args+=("-Dspring-boot.run.profiles=$profiles")
|
||||
setsid nohup mvn "${args[@]}" > /tmp/auth-server.log 2>&1 < /dev/null &
|
||||
wait_for "$AS/oauth2/jwks" 90
|
||||
}
|
||||
start_client() {
|
||||
kill_app ClientApplication 8080
|
||||
local profiles="${1:-}"
|
||||
local args=(-B -o -pl oidc-client org.springframework.boot:spring-boot-maven-plugin:run)
|
||||
[ -n "$profiles" ] && args+=("-Dspring-boot.run.profiles=$profiles")
|
||||
setsid nohup mvn "${args[@]}" > /tmp/oidc-client.log 2>&1 < /dev/null &
|
||||
wait_for "http://127.0.0.1:8080/" 90
|
||||
}
|
||||
start_rs() {
|
||||
kill_app ResourceServerApplication 8090
|
||||
local profiles="${1:-}"
|
||||
local args=(-B -o -pl resource-server org.springframework.boot:spring-boot-maven-plugin:run)
|
||||
[ -n "$profiles" ] && args+=("-Dspring-boot.run.profiles=$profiles")
|
||||
setsid nohup mvn "${args[@]}" > /tmp/rs.log 2>&1 < /dev/null &
|
||||
wait_for "$RS/public" 90
|
||||
}
|
||||
|
||||
echo "== ClientSettings / TokenSettings defaults, 1.5.8 vs 7.1.1"
|
||||
./scripts/settings-defaults.sh > /dev/null
|
||||
|
||||
echo "== the SAS 1.x configuration against 7.1.1 (compile only)"
|
||||
./scripts/compile-legacy.sh > /dev/null
|
||||
|
||||
echo "== default profile"
|
||||
start_auth ""
|
||||
start_rs ""
|
||||
./scripts/discovery.sh
|
||||
./scripts/client-credentials.sh as-client-credentials
|
||||
./scripts/authcode-pkce.sh as-authcode-pkce demo-spa
|
||||
./scripts/authcode-pkce.sh as-authcode-web demo-web
|
||||
|
||||
echo "== why a confidential Spring client does not send PKCE by default"
|
||||
./scripts/pkce-applier.sh > /dev/null
|
||||
|
||||
echo "== the real Spring OAuth2 client, end to end"
|
||||
start_client ""
|
||||
./scripts/client-flow.sh as-client-flow "client sends PKCE"
|
||||
# Restart the authorization server too, so the consent already granted above does not
|
||||
# short-circuit the second run.
|
||||
start_auth ""
|
||||
start_client "nopkce"
|
||||
./scripts/client-flow.sh as-client-flow-nopkce "confidential client, no PKCE - the Boot default"
|
||||
kill_app ClientApplication 8080
|
||||
|
||||
echo "== noclaims: the token customiser removed"
|
||||
start_auth "noclaims"
|
||||
./scripts/client-credentials.sh as-client-credentials-noclaims
|
||||
./scripts/authcode-pkce.sh as-authcode-noclaims demo-spa
|
||||
|
||||
echo "== nopkce: the public client no longer requires a verifier"
|
||||
start_auth "nopkce"
|
||||
# With a challenge present, the server still demands the verifier - requireProofKey only
|
||||
# controls whether a challenge is MANDATORY, not whether one that was sent is honoured.
|
||||
./scripts/authcode-pkce.sh as-authcode-nopkce demo-spa
|
||||
# Without any challenge at all, the code alone is enough. This is the actual exposure.
|
||||
NO_CHALLENGE=1 ./scripts/authcode-pkce.sh as-authcode-nochallenge demo-spa
|
||||
|
||||
echo "== the same request against a client that DOES require PKCE"
|
||||
start_auth ""
|
||||
NO_CHALLENGE=1 ./scripts/authcode-pkce.sh as-authcode-pkce-enforced demo-spa
|
||||
|
||||
echo "== noconsent: consent turned off"
|
||||
start_auth "noconsent"
|
||||
./scripts/authcode-pkce.sh as-authcode-noconsent demo-spa
|
||||
|
||||
echo "== entry point and the Accept header"
|
||||
rm -f ../docs/output/as-entrypoint-accept.txt
|
||||
start_auth "acceptall"
|
||||
./scripts/entrypoint-accept.sh "acceptall profile: setIgnoredMediaTypes NOT called"
|
||||
start_auth ""
|
||||
./scripts/entrypoint-accept.sh "default profile: setIgnoredMediaTypes(ALL) called"
|
||||
|
||||
echo "== opaque: reference tokens for the service client"
|
||||
start_auth "opaque"
|
||||
./scripts/client-credentials.sh as-client-credentials-opaque
|
||||
|
||||
echo "== audience validation off on the resource server"
|
||||
start_auth ""
|
||||
start_rs "noaud"
|
||||
./scripts/audience.sh
|
||||
|
||||
echo "== the contract tests"
|
||||
mvn -B -o -pl auth-server test 2>&1 | grep -E "Tests run:|^\[INFO\] Running" \
|
||||
> ../docs/output/as-test-run.txt || true
|
||||
|
||||
kill_app AuthServerApplication 9000
|
||||
kill_app ResourceServerApplication 8090
|
||||
kill_app ClientApplication 8080
|
||||
|
||||
echo "== resource server startup with no provider"
|
||||
./scripts/rs-startup-failure.sh > /dev/null
|
||||
|
||||
echo
|
||||
echo "docs/output:"
|
||||
ls -1 ../docs/output/as-*.txt
|
||||
28
authorization-server/scripts/run.sh
Executable file
28
authorization-server/scripts/run.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# ./scripts/run.sh <auth|rs|client> [profiles]
|
||||
#
|
||||
# ./scripts/run.sh auth the provider on :9000
|
||||
# ./scripts/run.sh auth nopkce the provider with PKCE not required
|
||||
# ./scripts/run.sh rs the API on :8090
|
||||
# ./scripts/run.sh client the relying party on :8080
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
. scripts/lib.sh
|
||||
|
||||
case "${1:-}" in
|
||||
auth) MODULE=auth-server; MAIN=AuthServerApplication; PORT=9000 ;;
|
||||
rs) MODULE=resource-server; MAIN=ResourceServerApplication; PORT=8090 ;;
|
||||
client) MODULE=oidc-client; MAIN=ClientApplication; PORT=8080 ;;
|
||||
*) echo "usage: $0 <auth|rs|client> [profiles]" >&2; exit 2 ;;
|
||||
esac
|
||||
PROFILES="${2:-}"
|
||||
|
||||
kill_app "$MAIN" "$PORT"
|
||||
LOG="/tmp/${MODULE}.log"
|
||||
ARGS=(-B -pl "$MODULE" org.springframework.boot:spring-boot-maven-plugin:run)
|
||||
[ -n "$PROFILES" ] && ARGS+=("-Dspring-boot.run.profiles=$PROFILES")
|
||||
|
||||
# Detached, so the script returns and the demo scripts can drive it.
|
||||
setsid nohup mvn "${ARGS[@]}" > "$LOG" 2>&1 < /dev/null &
|
||||
echo "starting $MODULE${PROFILES:+ [$PROFILES]} -> $LOG"
|
||||
wait_for "http://localhost:$PORT/" 120 && echo "$MODULE up on :$PORT"
|
||||
60
authorization-server/scripts/settings-defaults.sh
Executable file
60
authorization-server/scripts/settings-defaults.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles one tiny program twice - once against Spring Authorization Server 1.5.8, once
|
||||
# against 7.1.1 - and prints the defaults each version hands you. This is how the
|
||||
# requireProofKey change was found; no documentation was involved.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=../docs/output/as-settings-defaults.txt
|
||||
mkdir -p ../docs/output
|
||||
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
|
||||
mkdir -p "$WORK/old"
|
||||
|
||||
fetch() { # group artifact version
|
||||
curl -sL -o "$WORK/old/$2-$3.jar" \
|
||||
"https://repo1.maven.org/maven2/$1/$2/$3/$2-$3.jar"
|
||||
}
|
||||
for a in spring-security-oauth2-authorization-server:1.5.8 spring-security-oauth2-core:6.5.1 \
|
||||
spring-security-oauth2-jose:6.5.1 spring-security-core:6.5.1 \
|
||||
spring-security-oauth2-client:6.5.1 spring-security-web:6.5.1; do
|
||||
fetch org/springframework/security "${a%%:*}" "${a##*:}"
|
||||
done
|
||||
for a in spring-core:6.2.7 spring-jcl:6.2.7; do
|
||||
fetch org/springframework "${a%%:*}" "${a##*:}"
|
||||
done
|
||||
|
||||
mvn -B -q -pl auth-server dependency:build-classpath \
|
||||
-Dmdep.outputFile=/tmp/as-cp.txt -DincludeScope=compile >/dev/null 2>&1
|
||||
CP_NEW=$(cat /tmp/as-cp.txt)
|
||||
CP_OLD=$(ls "$WORK"/old/*.jar | tr '\n' ':')
|
||||
|
||||
{
|
||||
echo "# Defaults of ClientSettings.builder().build() and TokenSettings.builder().build(),"
|
||||
echo "# read out of the jars themselves rather than from documentation."
|
||||
echo "# Source: tools/SettingsDefaults.java"
|
||||
echo
|
||||
echo "=== Spring Authorization Server 1.5.8 (last release of the standalone project) ==="
|
||||
javac -nowarn -cp "$CP_OLD" -d "$WORK/out-old" tools/SettingsDefaults.java \
|
||||
&& java -cp "$CP_OLD:$WORK/out-old" SettingsDefaults
|
||||
echo
|
||||
echo "=== Spring Authorization Server 7.1.1 (inside Spring Security, Boot 4.1.1 BOM) ==="
|
||||
javac -nowarn -cp "$CP_NEW" -d "$WORK/out-new" tools/SettingsDefaults.java \
|
||||
&& java -cp "$CP_NEW:$WORK/out-new" SettingsDefaults
|
||||
|
||||
echo
|
||||
echo "# The same question on the CLIENT side. Source: tools/ClientPkceDefault.java"
|
||||
mvn -B -q -pl oidc-client dependency:build-classpath \
|
||||
-Dmdep.outputFile=/tmp/cl-cp.txt -DincludeScope=compile >/dev/null 2>&1
|
||||
CP_CLIENT=$(cat /tmp/cl-cp.txt)
|
||||
echo
|
||||
echo "=== spring-security-oauth2-client 6.5.1 ==="
|
||||
javac -nowarn -cp "$CP_OLD" -d "$WORK/co" tools/ClientPkceDefault.java \
|
||||
&& java -cp "$CP_OLD:$WORK/co" ClientPkceDefault
|
||||
echo
|
||||
echo "=== spring-security-oauth2-client 7.1.1 (Boot 4.1.1 BOM) ==="
|
||||
javac -nowarn -cp "$CP_CLIENT" -d "$WORK/cn" tools/ClientPkceDefault.java \
|
||||
&& java -cp "$CP_CLIENT:$WORK/cn" ClientPkceDefault
|
||||
echo
|
||||
echo "# Both sides flipped in the 7.x line. Spring-to-Spring therefore still works;"
|
||||
echo "# a 7.1 authorization server in front of a 6.x or hand-rolled client does not."
|
||||
} > "$OUT" 2>&1
|
||||
cat "$OUT"
|
||||
Reference in New Issue
Block a user