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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user