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.
111 lines
4.4 KiB
Bash
Executable File
111 lines
4.4 KiB
Bash
Executable File
#!/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"
|