OAUTH 2.0 · LAB
Classify token endpoint errors before deciding whether to retry
Produce each token endpoint error your tenant returns, classify it as retry, fix or alert, and correlate every call by request ID without logging a token. Exchange-specific failures are planned.
Partly readyUses your lab tenant
The lesson
Builds on: The problem token exchange solves.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.
- G6 Token exchange (RFC 8693)
Your progress
Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.
Sign in to start this lab and check your progress. Log in or create an account.
Authenticate the photo API with a wrong secret
Recorded as
oauth.tokenrejected (invalid_client) forlab-photo-api.Use a grant lab-printer is not allowed
Recorded as
oauth.tokenrejected (unauthorized_client) forlab-printer.Ask for a scope the photo API is not assigned
Recorded as
oauth.tokenrejected (invalid_scope) forlab-photo-api.Redeem an authorization code a second time
Recorded as
oauth.tokenrejected (code_replayed) forlab-printer.
Setup
You need
lab-photo-apiwith theclient_credentialsgrant and thephotos.readscope (from Try three ways to call the storage service), andlab-printerwithout theclient_credentialsgrant.photos.deletemust not be assigned tolab-photo-api. KeepCLIENT_ID/CLIENT_SECRETandAPI_ID/API_SECRETin the shell.Save the classifier. It makes one call, keeps the body in memory only, and logs status, error, request ID and decision, never a token:
cat > classify.sh <<'EOF'
classify() { # classify CURL_ARGS...: one token endpoint call, classified
local out status body err rid decision
out=$(curl -s -D h.txt -w '\n%{http_code}' "$@") || { echo '{"decision":"retry_once","reason":"network"}'; return; }
status=${out##*$'\n'}; body=${out%$'\n'*}
rid=$(sed -n 's/^[Xx]-[Rr]equest-[Ii][Dd]: *//p' h.txt | tr -d '\r'); err=$(jq -r '.error // empty' <<<"$body")
case "$status:$err" in
429:*) decision=wait_for_retry_after;;
5*) decision=retry_with_backoff;;
200:) decision=ok;;
401:*|*:invalid_client) decision=fix_client_credentials;;
*:unauthorized_client|*:unsupported_grant_type|*:invalid_target) decision=configuration_error_alert;;
*:invalid_request|*:invalid_grant) decision=request_or_subject_invalid_no_retry;;
*:invalid_scope) decision=do_not_retry;;
*) decision=stop;;
esac
jq -nc --arg s "$status" --arg e "$err" --arg r "$rid" --arg d "$decision" '{status: $s, error: $e, request_id: $r, decision: $d}' | tee -a classify.log
}
EOF
. ./classify.sh
Press Start on this page with Lab Photos selected.
Walkthrough
Produce one error of each class, in this order:
T="$ISSUER/oauth/token"
classify -u "$API_ID:$API_SECRET" "$T" -d grant_type=client_credentials -d grant_type=client_credentials
classify -u "$API_ID:not-the-real-secret" "$T" -d grant_type=client_credentials
classify -u "$CLIENT_ID:$CLIENT_SECRET" "$T" -d grant_type=client_credentials
classify -u "$API_ID:$API_SECRET" "$T" --data-urlencode grant_type=urn:ietf:params:oauth:grant-type:token-exchange
classify -u "$API_ID:$API_SECRET" "$T" -d grant_type=client_credentials -d scope=photos.delete
The lines read invalid_request (a repeated parameter), 401 invalid_client, unauthorized_client, unsupported_grant_type and invalid_scope, each with its decision.
Why it matters: each error reports a decision, and sending the same request again gets the same decision. Only a timeout, a dropped connection, a 5xx or a 429 is worth retrying, and then once or twice with a growing delay.
A code redeemed twice. Run a
lab-printercode flow forphotos.read(startbtl-lab callback, open the authorization URL, approve as Ava, checkstateandiss,read -rs CODE), then redeem it twice:
for i in 1 2; do classify -u "$CLIENT_ID:$CLIENT_SECRET" "$T" -d grant_type=authorization_code --data-urlencode "code=$CODE" \
--data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "code_verifier=$VERIFIER"; done; unset CODE
The first line is ok, the second invalid_grant. Lab Photos records code_replayed and revokes the tokens issued from that code.
Why it matters: unlike an authorization code, a subject token is not used up by an exchange, so a repeated exchange after a timeout simply issues another short-lived token. A repeated code is treated as a leak.
Correlate. For each line in
classify.log, find itsrequest_idin Lab Photos. The repeated-parameter and unsupported-grant refusals were rejected before any client authenticated, so they appear only in Logs (filter outcome rejected). The others appear in Audit with the client and the reason.
Why it matters: request IDs join records across services even when no token was issued, which is often all an operator needs to see that a policy or configuration changed.
Confirm nothing secret was kept:
grep -c 'eyJ' classify.log; grep -ci 'authorization' classify.log
Both print 0.
Why it matters: never log tokens, the Authorization header or an exchange body. A refused call is recorded the same way as a successful one: status, error, client and request ID.
Planned walkthrough
These steps need token exchange (G6).
Expiry in the middle of an operation. Exchange a printer token that has 20 seconds left, wait 25 seconds, and exchange again:
invalid_request, because the subject token has expired. The photo API answers the printer with401anderror="invalid_token"so the printer gets a fresh token. It does not fall back to its own client credentials, to forwarding the printer's token, or to a cached token for another user.A configuration error.
unauthorized_clientorinvalid_targetfrom an exchange is the photo API's own misconfiguration, so it answers the printer with a server error and alerts its operators, rather than a401that would send the printer to replace a good token.The exchange record. Lab Photos' Audit shows
oauth.tokenwithtoken_exchanged, naming the client, the subject, the subject token'sjti, the issuedjti, the target and the scope, and refused exchanges with their reason, never a token.
Break it
Run the
invalid_clientcall once more and read its decision:fix_client_credentials. A client that retried here would only add failed authentications. The token endpoint also has rate limits that answer429withRetry-After, which the classifier maps towait_for_retry_after. Do not run a retry loop against your tenant to see one.
Why it matters: a loop that tries other targets or scopes until something succeeds is worse than a plain loop, because it probes the server's policy.
Check your work
Press Check my progress. The checks look in Lab Photos for the wrong-secret invalid_client and the invalid_scope refusal for lab-photo-api, and the unauthorized_client and code_replayed refusals for lab-printer, in that order. classify.log holds a classified line with a request ID for each call.
Cleanup
Delete h.txt and classify.log.
Missing infrastructure
G6 Token exchange, for the planned steps and for exchange-specific
invalid_requestcauses such as an expired, revoked or untrusted subject token.