1
0

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:
2026-08-24 08:12:36 +05:30
parent 4dc45d5e00
commit e9381dc5be
89 changed files with 5237 additions and 9 deletions

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