Add the service-to-service module
This commit is contained in:
36
service-to-service/scripts/certs.sh
Executable file
36
service-to-service/scripts/certs.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate a throwaway CA, a server certificate for the downstream service, and two client
|
||||
# certificates - one signed by that CA and one signed by a different CA. Into target/, so
|
||||
# nothing here is committed and nothing here should ever be trusted.
|
||||
#
|
||||
# ./scripts/certs.sh
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
D=target/certs
|
||||
rm -rf "$D" && mkdir -p "$D"
|
||||
cd "$D"
|
||||
|
||||
gen_ca() { # gen_ca <name> <cn>
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||
-keyout "$1-ca.key" -out "$1-ca.crt" -subj "/CN=$2" 2>/dev/null
|
||||
}
|
||||
|
||||
sign() { # sign <ca> <name> <subject> [extfile-content]
|
||||
openssl req -newkey rsa:2048 -nodes -keyout "$2.key" -out "$2.csr" -subj "$3" 2>/dev/null
|
||||
if [ -n "${4:-}" ]; then printf '%s\n' "$4" > "$2.ext"; else : > "$2.ext"; fi
|
||||
openssl x509 -req -in "$2.csr" -CA "$1-ca.crt" -CAkey "$1-ca.key" -CAcreateserial \
|
||||
-out "$2.crt" -days 3650 -extfile "$2.ext" 2>/dev/null
|
||||
}
|
||||
|
||||
gen_ca internal "Internal Mesh CA"
|
||||
gen_ca other "Some Other CA"
|
||||
|
||||
sign internal server "/CN=localhost" "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||
sign internal edge "/CN=edge-service/OU=payments"
|
||||
sign other rogue "/CN=edge-service/OU=payments"
|
||||
|
||||
echo "wrote:"
|
||||
ls -1 *.crt *.key | sed 's/^/ target\/certs\//'
|
||||
echo
|
||||
echo "Note that rogue.crt carries the SAME subject as edge.crt. Identity in mTLS is not the"
|
||||
echo "subject; it is the subject plus the fact that a trusted CA vouched for it."
|
||||
12
service-to-service/scripts/claims.sh
Executable file
12
service-to-service/scripts/claims.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Print the header and payload of a JWT without verifying it. For reading, not for trusting.
|
||||
#
|
||||
# ./scripts/claims.sh "$TOKEN"
|
||||
set -eu
|
||||
python3 - "$1" <<'PY'
|
||||
import base64, json, sys
|
||||
token = sys.argv[1]
|
||||
for part in token.split('.')[:2]:
|
||||
padded = part + '=' * (-len(part) % 4)
|
||||
print(json.dumps(json.loads(base64.urlsafe_b64decode(padded)), indent=2, sort_keys=True))
|
||||
PY
|
||||
214
service-to-service/scripts/run-all.sh
Executable file
214
service-to-service/scripts/run-all.sh
Executable file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file under docs/output/ from a real run. Nothing in docs/output/ is
|
||||
# hand-written; if a claim in the article disagrees with a file here, the file is right.
|
||||
#
|
||||
# ./scripts/run-all.sh
|
||||
#
|
||||
# The whole module is started twice - once loose, once strict - plus a separate mTLS process,
|
||||
# because the difference between those runs IS the content.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=docs/output
|
||||
mkdir -p "$OUT"
|
||||
|
||||
hdr() { printf '%s\n%s\n%s\n\n' "$(printf '=%.0s' $(seq 1 78))" "$1" "$(printf '=%.0s' $(seq 1 78))"; }
|
||||
|
||||
scrub() {
|
||||
sed -E \
|
||||
-e 's/\r$//' \
|
||||
-e 's/[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9:.]+(Z|\+[0-9:]+)?/<timestamp>/g' \
|
||||
-e 's/"(exp|iat|nbf)": [0-9]+/"\1": <epoch>/g' \
|
||||
-e 's/"(jti|kid)": "[0-9a-f-]+"/"\1": "<uuid>"/g' \
|
||||
-e 's/(JSESSIONID=)[0-9A-F]+/\1<session>/g' \
|
||||
-e 's/issuedAt=[^]]*\]/issuedAt=<timestamp>]/g' \
|
||||
-e 's/ [0-9]+ --- / <pid> --- /g' \
|
||||
-e '/Picked up JAVA_TOOL_OPTIONS/d' \
|
||||
| cat -s
|
||||
}
|
||||
|
||||
claims() { ./scripts/claims.sh "$1"; }
|
||||
|
||||
json() { python3 -m json.tool 2>/dev/null || cat; }
|
||||
|
||||
cc_token() { # cc_token <client> <secret>
|
||||
curl -s -u "$1:$2" -X POST http://127.0.0.1:9000/oauth2/token \
|
||||
-d grant_type=client_credentials -d scope=orders.read \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("access_token",""))'
|
||||
}
|
||||
|
||||
########################################################################################
|
||||
# LOOSE RUN
|
||||
########################################################################################
|
||||
./scripts/run.sh > /dev/null 2>&1
|
||||
USER_TOKEN=$(./scripts/user-token.sh)
|
||||
|
||||
{
|
||||
hdr "docs/output/01-user-token.txt
|
||||
A complete authorization_code + PKCE flow, driven by curl. No browser, no OIDC library.
|
||||
scripts/user-token.sh, then scripts/claims.sh"
|
||||
claims "$USER_TOKEN"
|
||||
echo
|
||||
echo "# sub is the human. scope is what the human consented to. aud names the service the"
|
||||
echo "# token was minted for - chapter 4 is about whether anybody looks at it."
|
||||
} | scrub > "$OUT/01-user-token.txt"
|
||||
|
||||
{
|
||||
hdr "docs/output/02-five-strategies.txt
|
||||
The same request into the edge service, five ways of getting a token for the hop to
|
||||
downstream. Read the sub and scope of each downstream response.
|
||||
GET /edge/{naive,relay,client-credentials,exchange,relay-async}"
|
||||
for endpoint in naive relay client-credentials exchange relay-async; do
|
||||
echo "\$ curl -H \"Authorization: Bearer \$TOKEN\" 127.0.0.1:8081/edge/$endpoint"
|
||||
curl -s -H "Authorization: Bearer $USER_TOKEN" "http://127.0.0.1:8081/edge/$endpoint" | json
|
||||
echo
|
||||
done
|
||||
echo "# naive - 401. The control."
|
||||
echo "# relay - sub: alice, scope: [orders.write, orders.read]. The user's own"
|
||||
echo "# token, unchanged, including scopes downstream did not need."
|
||||
echo "# client-creds - sub: edge-service, scope: [orders.read]. Correctly scoped, and"
|
||||
echo "# the user has disappeared from downstream's audit log."
|
||||
echo "# exchange - sub: alice, scope: [orders.read]. Both. This is what RFC 8693 is"
|
||||
echo "# for and it is the one nobody reaches for."
|
||||
echo "# relay-async - 401. The relay interceptor reads SecurityContextHolder, which is"
|
||||
echo "# a ThreadLocal, and the call was made on a different thread."
|
||||
} | scrub > "$OUT/02-five-strategies.txt"
|
||||
|
||||
{
|
||||
hdr "docs/output/03-audience-ignored.txt
|
||||
A token minted for a DIFFERENT service, presented to the downstream service.
|
||||
Default validators."
|
||||
WRONG=$(cc_token reporting-service reporting-secret)
|
||||
echo "# the token reporting-service was issued:"
|
||||
claims "$WRONG"
|
||||
echo
|
||||
echo "\$ curl -H 'Authorization: Bearer <reporting-service token>' 127.0.0.1:8082/orders"
|
||||
curl -s -H "Authorization: Bearer $WRONG" http://127.0.0.1:8082/orders | json
|
||||
echo
|
||||
echo "# HTTP 200. The aud claim says reporting-api. The service is downstream-api."
|
||||
echo "# JwtValidators.createDefault() is a DelegatingOAuth2TokenValidator over three"
|
||||
echo "# validators - JwtTypeValidator, JwtTimestampValidator and"
|
||||
echo "# X509CertificateThumbprintValidator. Structure, expiry, and certificate binding."
|
||||
echo "# No issuer. No audience. Read back by reflection in ValidatorContractTests."
|
||||
} | scrub > "$OUT/03-audience-ignored.txt"
|
||||
|
||||
{
|
||||
hdr "docs/output/04-gateway-token-relay.txt
|
||||
Spring Cloud Gateway Server MVC with 'filters: - TokenRelay='.
|
||||
GET /edge/relay through the gateway on 8080."
|
||||
echo "\$ curl -H \"Authorization: Bearer \$TOKEN\" 127.0.0.1:8080/edge/relay"
|
||||
curl -s -i -H "Authorization: Bearer $USER_TOKEN" http://127.0.0.1:8080/edge/relay \
|
||||
| sed -n '1,/^\r$/p' | grep -viE '^(date|keep-alive|connection|content-length|transfer-encoding):'
|
||||
curl -s -H "Authorization: Bearer $USER_TOKEN" http://127.0.0.1:8080/edge/relay | json
|
||||
echo
|
||||
echo "\$ curl 127.0.0.1:8080/edge/relay # no Authorization header at all"
|
||||
curl -s -o /dev/null -w 'status %{http_code}\n' http://127.0.0.1:8080/edge/relay
|
||||
echo
|
||||
echo "# and the identical route with the TokenRelay filter REMOVED:"
|
||||
echo "\$ curl -H \"Authorization: Bearer \$TOKEN\" 127.0.0.1:8080/norelay/x"
|
||||
curl -s -H "Authorization: Bearer $USER_TOKEN" http://127.0.0.1:8080/norelay/x | json
|
||||
echo
|
||||
echo "# TokenRelay relays the access token of the currently authenticated USER - the one"
|
||||
echo "# obtained by oauth2Login(). This gateway has no oauth2Login, so there is no"
|
||||
echo "# authorized client to read a token from, and the filter contributes nothing. What"
|
||||
echo "# reaches the edge service is whatever Authorization header the caller sent, because"
|
||||
echo "# the gateway proxied it. TokenRelay is not 'forward the incoming bearer token'."
|
||||
} | scrub > "$OUT/04-gateway-token-relay.txt"
|
||||
|
||||
########################################################################################
|
||||
# STRICT RUN
|
||||
########################################################################################
|
||||
STRICT=true ./scripts/run.sh > /dev/null 2>&1
|
||||
{
|
||||
hdr "docs/output/05-strict-validation.txt
|
||||
The same tokens against JwtValidators.createAtJwtValidator().issuer(..).audience(..),
|
||||
with the authorization server emitting RFC 9068 tokens (typ: at+jwt, client_id claim).
|
||||
STRICT=true ./scripts/run.sh"
|
||||
WRONG=$(cc_token reporting-service reporting-secret)
|
||||
RIGHT=$(cc_token edge-service edge-secret)
|
||||
echo "# the wrong-audience token that was accepted in 03:"
|
||||
curl -s -i -H "Authorization: Bearer $WRONG" http://127.0.0.1:8082/orders \
|
||||
| grep -iE '^(HTTP|WWW-Authenticate)'
|
||||
echo
|
||||
echo "# a token minted for this service:"
|
||||
curl -s -H "Authorization: Bearer $RIGHT" http://127.0.0.1:8082/orders | json
|
||||
echo
|
||||
echo "# and the edge service, which was NOT updated - it still uses Boot's"
|
||||
echo "# auto-configured decoder:"
|
||||
STRICT_USER=$(./scripts/user-token.sh)
|
||||
curl -s -i -H "Authorization: Bearer $STRICT_USER" http://127.0.0.1:8081/edge/relay \
|
||||
| grep -iE '^(HTTP|WWW-Authenticate)'
|
||||
echo
|
||||
echo "# Turning on RFC 9068 at the authorization server broke every resource server that"
|
||||
echo "# still has NimbusJwtDecoder's default JOSE type verifier, and the error message"
|
||||
echo "# mentions neither RFC 9068 nor the authorization server."
|
||||
} | scrub > "$OUT/05-strict-validation.txt"
|
||||
|
||||
./scripts/stop.sh
|
||||
|
||||
########################################################################################
|
||||
# mTLS
|
||||
########################################################################################
|
||||
./scripts/certs.sh > /dev/null
|
||||
CP="target/classes:$(cat target/cp.txt)"
|
||||
setsid nohup java -Xmx160m -cp "$CP" com.ankurm.s2s.mtls.MtlsApplication \
|
||||
> /tmp/s2s-Mtls.log 2>&1 < /dev/null &
|
||||
for _ in $(seq 1 60); do
|
||||
curl -s -o /dev/null -m 2 --cacert target/certs/internal-ca.crt \
|
||||
--cert target/certs/edge.crt --key target/certs/edge.key \
|
||||
https://localhost:8443/mtls/whoami && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
{
|
||||
hdr "docs/output/06-mtls.txt
|
||||
Client-certificate authentication on port 8443, server.ssl.client-auth=need.
|
||||
Certificates from scripts/certs.sh. edge.crt and rogue.crt have IDENTICAL subjects and
|
||||
different issuers."
|
||||
echo "\$ openssl x509 -in target/certs/edge.crt -noout -subject -issuer"
|
||||
openssl x509 -in target/certs/edge.crt -noout -subject -issuer
|
||||
echo "\$ openssl x509 -in target/certs/rogue.crt -noout -subject -issuer"
|
||||
openssl x509 -in target/certs/rogue.crt -noout -subject -issuer
|
||||
echo
|
||||
echo "\$ curl --cert edge.crt --key edge.key https://localhost:8443/mtls/whoami"
|
||||
curl -s --cacert target/certs/internal-ca.crt --cert target/certs/edge.crt \
|
||||
--key target/certs/edge.key https://localhost:8443/mtls/whoami | json
|
||||
echo
|
||||
echo "\$ curl --cert rogue.crt --key rogue.key https://localhost:8443/mtls/whoami"
|
||||
curl -s --cacert target/certs/internal-ca.crt --cert target/certs/rogue.crt \
|
||||
--key target/certs/rogue.key https://localhost:8443/mtls/whoami \
|
||||
-w '[curl exit %{exitcode}, http %{http_code}]\n' 2>&1 | tail -1
|
||||
echo
|
||||
echo "\$ curl https://localhost:8443/mtls/trusted-header # a permitAll() endpoint"
|
||||
curl -sk https://localhost:8443/mtls/trusted-header \
|
||||
-w '[curl exit %{exitcode}, http %{http_code}]\n' 2>&1 | tail -1
|
||||
echo
|
||||
echo "\$ curl --cert edge.crt --key edge.key -H 'X-Client-Cert-Subject: CN=payments-service' \\"
|
||||
echo " https://localhost:8443/mtls/trusted-header"
|
||||
curl -s --cacert target/certs/internal-ca.crt --cert target/certs/edge.crt \
|
||||
--key target/certs/edge.key -H 'X-Client-Cert-Subject: CN=payments-service' \
|
||||
https://localhost:8443/mtls/trusted-header
|
||||
echo
|
||||
echo
|
||||
echo "# Three things worth reading twice."
|
||||
echo "# 1. The rogue certificate fails with curl exit 56 and NO http status. The handshake"
|
||||
echo "# is rejected; the application never sees a request and logs nothing at INFO."
|
||||
echo "# 2. So does the permitAll() endpoint. client-auth=need is a property of the"
|
||||
echo "# CONNECTOR, not of a path. You cannot expose a public endpoint on that port."
|
||||
echo "# 3. The last call is what a mesh deployment usually looks like from inside the"
|
||||
echo "# application: an identity taken from a header, verified by nothing."
|
||||
} | scrub > "$OUT/06-mtls.txt"
|
||||
|
||||
for pid in $(ps -eo pid,ppid,comm,args | awk '$3 ~ /^java/ && $0 ~ /com\.ankurm\.s2s\.mtls/ {print $1}'); do
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
done
|
||||
|
||||
########################################################################################
|
||||
# TESTS
|
||||
########################################################################################
|
||||
{
|
||||
hdr "docs/output/07-tests.txt
|
||||
mvn -B test"
|
||||
mvn -B test 2>&1 | grep -E 'Tests run|ERROR|BUILD' | head -30
|
||||
} | scrub > "$OUT/07-tests.txt"
|
||||
|
||||
echo "regenerated $(ls "$OUT" | wc -l) files under $OUT"
|
||||
64
service-to-service/scripts/run.sh
Executable file
64
service-to-service/scripts/run.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start all four processes and wait until each answers.
|
||||
#
|
||||
# ./scripts/run.sh
|
||||
# STRICT=true ./scripts/run.sh # downstream validates issuer and audience
|
||||
# RS_LOG_LEVEL=DEBUG ./scripts/run.sh # resource-server decisions at DEBUG
|
||||
#
|
||||
# | Process | Port | Main class |
|
||||
# |-------------|------|---------------------------|
|
||||
# | authserver | 9000 | AuthServerApplication |
|
||||
# | gateway | 8080 | GatewayApplication |
|
||||
# | edge | 8081 | EdgeApplication |
|
||||
# | downstream | 8082 | DownstreamApplication |
|
||||
#
|
||||
# These are launched with plain `java`, not `spring-boot:run`. Four Maven JVMs each forking an
|
||||
# application JVM is eight processes, and on a small machine that is how you meet the OOM
|
||||
# killer rather than the demo. `mvn dependency:build-classpath` once, then `java -cp` four
|
||||
# times, is two hundred megabytes of heap instead of two gigabytes.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
./scripts/stop.sh
|
||||
mvn -B -q compile
|
||||
if [ ! -f target/cp.txt ]; then
|
||||
mvn -B -q dependency:build-classpath -Dmdep.outputFile=target/cp.txt -Dmdep.includeScope=runtime
|
||||
fi
|
||||
CP="target/classes:$(cat target/cp.txt)"
|
||||
|
||||
start() { # start <main-class> <port> <health-path> <extra-jvm-args...>
|
||||
local main="$1" port="$2" path="$3"
|
||||
shift 3
|
||||
setsid nohup java -Xmx192m -XX:TieredStopAtLevel=1 "$@" ${JVM_ARGS:-} \
|
||||
-cp "$CP" "com.ankurm.s2s.$main" \
|
||||
> "/tmp/s2s-${main##*.}.log" 2>&1 < /dev/null &
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -s -o /dev/null "http://127.0.0.1:$port$path" 2>/dev/null; then
|
||||
echo " ${main##*.} up on $port"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "${main##*.} did not start; see /tmp/s2s-${main##*.}.log" >&2
|
||||
tail -20 "/tmp/s2s-${main##*.}.log" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# STRICT=true turns on RFC 9068 end to end: the authorization server types its access tokens
|
||||
# `at+jwt` and adds a client_id claim, and the downstream service validates issuer, audience
|
||||
# and the required-claim set instead of only signature and expiry.
|
||||
STRICT_ARGS=()
|
||||
AS_ARGS=()
|
||||
if [ "${STRICT:-false}" = "true" ]; then
|
||||
STRICT_ARGS=(-DSTRICT=true)
|
||||
AS_ARGS=(-DAT_JWT=true)
|
||||
fi
|
||||
|
||||
# The authorization server must be first and must be READY before the others: `edge` is an
|
||||
# OAuth2 client, and a client whose provider is configured with issuer-uri fetches the
|
||||
# discovery document during context refresh. See docs/01-the-four-processes.md.
|
||||
start authserver.AuthServerApplication 9000 /oauth2/jwks "${AS_ARGS[@]}"
|
||||
start downstream.DownstreamApplication 8082 /orders "${STRICT_ARGS[@]}"
|
||||
start edge.EdgeApplication 8081 /edge/naive
|
||||
start gateway.GatewayApplication 8080 /edge/naive
|
||||
echo "all four up"
|
||||
23
service-to-service/scripts/stop.sh
Executable file
23
service-to-service/scripts/stop.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop every process in the module.
|
||||
#
|
||||
# Two rules, both learned the hard way while building this repository:
|
||||
#
|
||||
# 1. Match the MAIN CLASS, never 'spring-boot' or 'java'. `pkill -f spring-boot` also matches
|
||||
# the shell command line that launched the application, so it kills your own shell.
|
||||
#
|
||||
# 2. Restrict the match to processes that are actually a JVM, and exclude this script and its
|
||||
# parent. A bracketed pattern like '[A]uthServerApplication' stops the pattern matching the
|
||||
# grep itself - but it does NOT stop it matching an ancestor shell whose command line
|
||||
# happens to contain that string, which is exactly what happens when you paste a here-doc
|
||||
# containing the class name into a terminal and then run this script from it. The victim
|
||||
# process dies with exit 137 and no output, which is a memorable afternoon.
|
||||
set -eu
|
||||
SELF=$$
|
||||
PARENT=$PPID
|
||||
ps -eo pid,ppid,comm,args | awk -v self="$SELF" -v parent="$PARENT" '
|
||||
$1 != self && $1 != parent && $3 ~ /^java/ && $0 ~ /com\.ankurm\.s2s\./ { print $1 }
|
||||
' | while read -r pid; do
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
done
|
||||
sleep 1
|
||||
48
service-to-service/scripts/user-token.sh
Executable file
48
service-to-service/scripts/user-token.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drive a complete authorization_code + PKCE flow with curl and print the access token.
|
||||
#
|
||||
# TOKEN=$(./scripts/user-token.sh)
|
||||
#
|
||||
# There is no browser here and none is needed: the "browser flow" is four HTTP requests and a
|
||||
# cookie jar. Doing it by hand once is the fastest way to understand what your SPA's OIDC
|
||||
# library is actually doing, and it makes every transcript in docs/output/ reproducible.
|
||||
set -eu
|
||||
AS=http://127.0.0.1:9000
|
||||
JAR=$(mktemp)
|
||||
trap 'rm -f "$JAR"' EXIT
|
||||
|
||||
# 1. PKCE: a random verifier, and its base64url-encoded SHA-256 as the challenge.
|
||||
VERIFIER=$(head -c 48 /dev/urandom | base64 | tr -d '=+/' | cut -c1-64)
|
||||
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -binary -sha256 | base64 | tr '+/' '-_' | tr -d '=')
|
||||
|
||||
AUTHORIZE="$AS/oauth2/authorize?response_type=code&client_id=spa-client\
|
||||
&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fauthorized&scope=orders.read%20orders.write\
|
||||
&code_challenge=$CHALLENGE&code_challenge_method=S256"
|
||||
|
||||
# 2. Ask for the code. Unauthenticated, so this saves the request and redirects to /login.
|
||||
curl -s -o /dev/null -c "$JAR" -b "$JAR" "$AUTHORIZE"
|
||||
|
||||
# 3. Log in. The login page is CSRF-protected, so read the token out of the form.
|
||||
CSRF=$(curl -s -c "$JAR" -b "$JAR" "$AS/login" \
|
||||
| grep -oiE 'name="_csrf"[^>]*value="[^"]*"' | head -1 | sed 's/.*value="//; s/"//')
|
||||
curl -s -o /dev/null -c "$JAR" -b "$JAR" -X POST "$AS/login" \
|
||||
-d "username=alice" -d "password=password" -d "_csrf=$CSRF"
|
||||
|
||||
# 4. Follow the saved request. Now authenticated, so this redirects to the redirect_uri
|
||||
# carrying ?code=... We never let curl follow it; we just read the Location header.
|
||||
CODE=$(curl -s -o /dev/null -D- -c "$JAR" -b "$JAR" "$AUTHORIZE" \
|
||||
| grep -i '^location:' | sed 's/.*code=//; s/[&\r].*//')
|
||||
|
||||
if [ -z "$CODE" ]; then
|
||||
echo "no authorization code was issued - is the auth server up?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Redeem it. A public client, so no client secret: the code_verifier is the proof.
|
||||
curl -s -X POST "$AS/oauth2/token" \
|
||||
-d grant_type=authorization_code \
|
||||
-d "code=$CODE" \
|
||||
-d "redirect_uri=http://127.0.0.1:8080/authorized" \
|
||||
-d client_id=spa-client \
|
||||
-d "code_verifier=$VERIFIER" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])'
|
||||
Reference in New Issue
Block a user