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

Run one printer connection and label every message

Run the whole connection once as the printer's backend, keeping a pending transaction, then label each Audit event as a browser message or a direct one.

ReadyUses your lab tenant

The lesson

Builds on: Why use the authorization code flow?, Trust boundaries.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

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. Ben approves and the browser returns a code

    Recorded as oauth.authorize succeeded (code_issued) for lab-printer about [email protected].

  2. The printer's backend exchanges the code

    Recorded as oauth.token succeeded for lab-printer about [email protected].

  3. The photo API checks the printer's token

    Recorded as oauth.introspect succeeded (other_client_token_found) for lab-photo-api about [email protected].

  4. A second attempt overwrote the first one's verifier

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

Setup

  1. Set the shell variables for lab-printer and for lab-photo-api, which plays the photo API through btl-lab introspect. Then press Start on this page.

export ISSUER="https://tenant-<id>.beyondthelogin.dev"
export CLIENT_ID="<lab-printer client ID>"; read -rs CLIENT_SECRET
export API_ID="<lab-photo-api client ID>"; read -rs API_SECRET && export API_SECRET
export REDIRECT_URI="https://beyondthelogin.dev/lab/callback/"
enc() { jq -rn --arg v "$1" '$v|@uri'; }

Ben has not used the printer yet, so he will see a consent page.

Walkthrough

  1. Prepare the connection. The printer reads its endpoints from trusted configuration once, not from anything that arrives with a browser request. Your shell session is the printer's pending transaction: it holds state and the PKCE verifier before the browser leaves.

TOKEN_ENDPOINT=$(curl -s "$ISSUER/.well-known/openid-configuration" | jq -r .token_endpoint)
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; EXPECTED_ISSUER="$ISSUER"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(enc "$REDIRECT_URI")&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256&prompt=login"

Why it matters: the lesson's printer creates its pending transaction when you select Connect. Everything it needs to finish this one attempt exists before the photo service sees the request.

  1. Visit the photo service: open the address in a private window. prompt=login forces the sign-in page. Sign in as Ben and approve on the consent page.

  1. Return to the printer. Copy the three values from the callback, then check that the response belongs to this attempt.

read -r GOT_STATE; read -r GOT_ISS; read -r CODE
[ "$GOT_STATE" = "$STATE" ] && [ "$GOT_ISS" = "$EXPECTED_ISSUER" ] && echo "belongs to this attempt"
  1. Finish the exchange at the token endpoint from step 1, with the printer's secret and this attempt's verifier.

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code -d "code=$CODE" \
  --data-urlencode "redirect_uri=$REDIRECT_URI" -d "code_verifier=$VERIFIER" "$TOKEN_ENDPOINT" | jq

Copy the access token: read -r TOKEN.

  1. The photo API decides. btl-lab introspect "$TOKEN" shows Ben's sub, the printer's client_id and scope: "photos.read".

  1. Open Audit and label each event from this run.

EventCarried by
oauth.authorize request_startedthe browser, arriving at the authorization endpoint
oauth.authorize user_signed_in, actor Benthe browser
oauth.authorize code_issuedthe browser, carrying the code back to the printer
oauth.token succeeded, actor lab-printera direct request from the printer's backend to the token endpoint
oauth.introspect other_client_token_found, actor lab-photo-apia direct request from the API to the authorization server

In Logs, oauth.authorize succeeded consent_required counted the consent page Ben saw.

Why it matters: following who sends each message is why OAuth uses separate endpoints. The browser carries the request and the code; the backend carries the secret, the verifier and the token.

Break it

  1. Run step 1 and open the address in a tab, but do not approve yet. This is attempt A.

  2. Select "Connect" again before finishing: run step 1 once more in the same terminal. Attempt B overwrites STATE and VERIFIER.

  3. Go back to attempt A's tab, approve, copy its code into CODE, and run step 4. The token endpoint answers invalid_grant with "code_verifier does not match the code_challenge sent in the authorization request."

One record per attempt keeps one tab from overwriting another's verifier. The Correlating requests and responses lab builds that.

Check your work

Press Check my progress. The Audit chain in step 6, all with subject Ben, is the evidence for the connection.

Cleanup

None. Errors and denied access repeats this exchange as the section's final run, with every check in place.

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