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

Build the printer's token request by hand

Write the Basic header yourself, send the form-encoded token request, read every field of the response, and see the checks only the token endpoint can make.

ReadyUses your lab tenant

The lesson

Builds on: Correlating requests and responses.

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. Exchange the code with a hand-built request

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

  2. The photo API validates the printer's token

    Recorded as oauth.introspect succeeded (other_client_token_found) for lab-photo-api.

  3. A different redirect_uri at the token endpoint is refused

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

  4. The same code still works with the right redirect_uri

    Recorded as oauth.token succeeded for lab-printer.

  5. A token request without redirect_uri is refused

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

Setup

  1. Set the lab-printer variables, plus API_ID and API_SECRET for lab-photo-api, and press Start on this page.

  2. Define a quick connection and get a code for Ava.

connect() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
  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"; }
connect

Open the address, sign in as Ava, approve, check that state and iss match your attempt, and read -r CODE.

Walkthrough

  1. Build the client authentication header the way the lesson shows it.

BASIC=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | openssl base64 -A)
printf '%s' "$BASIC" | openssl base64 -d -A | cut -d: -f1   # prints the client ID

Why it matters: Base64 is an encoding, not encryption. Anyone who sees the header can read the secret, which is why the request must travel over HTTPS and must never be logged in full.

  1. Send the request with every header visible.

curl -s -i -H "Authorization: Basic $BASIC" -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode grant_type=authorization_code --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=$REDIRECT_URI" --data-urlencode "code_verifier=$VERIFIER" "$ISSUER/oauth/token"

The answer is 200 with Content-Type: application/json, Cache-Control: no-store, Pragma: no-cache, and a body with access_token, token_type: "Bearer", expires_in and scope: "photos.read". Copy the access token: read -r TOKEN.

Why it matters: the body is form-encoded, not JSON. redirect_uri repeats the earlier value so the server can compare it; it does not cause another redirect.

  1. Read the response the way the printer must.

    • token_type: Bearer, so the token goes in an Authorization: Bearer header.

    • expires_in: store an expiry time, echo $(( $(date +%s) + <expires_in> )), rather than the relative number.

    • scope: what was actually granted, which can be less than the request.

    • no refresh_token: this connection did not ask for continued access. The refresh token lab adds one.

  1. Use the result. Start the toolkit's photo API in a second terminal with btl-lab resource --mode introspect, then make the lesson's API request.

curl -s -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8766/photos

The answer is 200. The resource server introspected the token as lab-photo-api before answering.

Why it matters: the token endpoint's success is not the API's decision. The API still validates the token for its own use and checks the operation.

Break it

  1. A mismatched redirect URI. Run connect, approve as Ava, read -r CODE, then exchange with a different loopback port than the authorization request used.

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code -d "code=$CODE" \
  --data-urlencode "redirect_uri=http://127.0.0.1:9999/callback" -d "code_verifier=$VERIFIER" "$ISSUER/oauth/token" | jq

The answer is invalid_grant, "redirect_uri must exactly match the value sent in the authorization request." Loopback ports may vary between authorization requests, but the exchange must repeat the exact value from its own request.

  1. Repeat the exchange with --data-urlencode "redirect_uri=$REDIRECT_URI": 200. The refused attempt did not use up the code.

  1. JSON instead of a form: send -H "Content-Type: application/json" -d '{"grant_type":"authorization_code"}' with the printer's credentials. The answer is 400 invalid_request, "Send a POST with an application/x-www-form-urlencoded body, no query string, and each parameter at most once."

  1. Leave out redirect_uri on a fresh code (connect again). The answer is 400 invalid_request, "The request is missing a required parameter or repeats a parameter."

Check your work

Press Check my progress. Logs also has oauth.token rejected malformed_request with status 400 for the JSON body in Break it, which the tenant refuses before it identifies the client.

Cleanup

Stop the resource server. Nothing in the tenant changed.

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