Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

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.

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.

  1. Authenticate the photo API with a wrong secret

    Recorded as oauth.token rejected (invalid_client) for lab-photo-api.

  2. Use a grant lab-printer is not allowed

    Recorded as oauth.token rejected (unauthorized_client) for lab-printer.

  3. Ask for a scope the photo API is not assigned

    Recorded as oauth.token rejected (invalid_scope) for lab-photo-api.

  4. Redeem an authorization code a second time

    Recorded as oauth.token rejected (code_replayed) for lab-printer.

Setup

  1. You need lab-photo-api with the client_credentials grant and the photos.read scope (from Try three ways to call the storage service), and lab-printer without the client_credentials grant. photos.delete must not be assigned to lab-photo-api. Keep CLIENT_ID/CLIENT_SECRET and API_ID/API_SECRET in the shell.

  2. 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
  1. Press Start on this page with Lab Photos selected.

Walkthrough

  1. 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.

  1. A code redeemed twice. Run a lab-printer code flow for photos.read (start btl-lab callback, open the authorization URL, approve as Ava, check state and iss, 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.

  1. Correlate. For each line in classify.log, find its request_id in 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.

  1. 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).

  1. 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 with 401 and error="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.

  2. A configuration error. unauthorized_client or invalid_target from an exchange is the photo API's own misconfiguration, so it answers the printer with a server error and alerts its operators, rather than a 401 that would send the printer to replace a good token.

  3. The exchange record. Lab Photos' Audit shows oauth.token with token_exchanged, naming the client, the subject, the subject token's jti, the issued jti, the target and the scope, and refused exchanges with their reason, never a token.

Break it

  1. Run the invalid_client call 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 answer 429 with Retry-After, which the classifier maps to wait_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_request causes such as an expired, revoked or untrusted subject token.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab