OAUTH 2.0 · LAB
Register the printer twice, as a confidential and a public client
Create lab-printer and lab-printer-app, then see that a client ID proves nothing, a secret authenticates the confidential client, and PKCE protects the public client's exchange.
ReadyUses your lab tenant
The lesson
Builds on: Grants, scopes, and consent.
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.
Sign in to start this lab and check your progress. Log in or create an account.
Register the printer's clients
Recorded as
tenant.oauth.clients.createsucceeded.A code presented with only the printer's client ID is refused
Recorded as
oauth.tokenrejected (unsupported_auth_method) forlab-printer.The printer authenticates and receives a token
Recorded as
oauth.tokensucceeded forlab-printer.The public app's request without PKCE is refused
Recorded as
oauth.authorizerejected (pkce_required) forlab-printer-app.The public app redeems its code with a PKCE verifier
Recorded as
oauth.tokensucceeded forlab-printer-app.
Setup
Press Start on this page.
Open OAuth > Clients > Create client and start from Web application. Name it
lab-printer, type Confidential, PKCE for confidential clients Required, Consent Ask once per set of scopes. Add two redirect URIs:http://127.0.0.1:8765/callbackandhttps://beyondthelogin.dev/lab/callback/. Tick Enabled and save.Open Access Token Management > Client authentication secret, choose
lab-printerand Rotate client secret. The secret is shown once.Create a second client from Native or desktop application. Name it
lab-printer-app, type Public, redirect URIhttp://127.0.0.1/callback, tick Enabled and save.Set the shell variables.
REDIRECT_URIis where the printer's codes return: the hosted callback page displayscode,stateandissfor you to copy, or use the loopback address withbtl-lab callbackrunning in a second terminal.
export ISSUER="https://tenant-<id>.beyondthelogin.dev"
export CLIENT_ID="<lab-printer client ID>"
read -rs CLIENT_SECRET # the lab-printer secret
export APP_ID="<lab-printer-app client ID>"
export REDIRECT_URI="https://beyondthelogin.dev/lab/callback/" # or http://127.0.0.1:8765/callback
enc() { jq -rn --arg v "$1" '$v|@uri'; } # URL-encodes one value
Walkthrough
Open Access Token Management > Client authentication secret again. The selector lists
lab-printerandlab-photo-api, but notlab-printer-app.
Why it matters: client type describes whether the deployment can protect a credential. Code delivered to a browser or packaged into every installed copy cannot, so the tenant issues no secret to the public client.
Start an authorization for the printer. Anyone can build this address: the client ID is in plain sight.
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"
Open the printed address, sign in as Ava and approve. Copy the code from the callback: read -r CODE.
Present the code with only the client ID, as anyone who knew the ID could.
curl -s -i -d grant_type=authorization_code -d "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT_URI" \
-d "code_verifier=$VERIFIER" -d "client_id=$CLIENT_ID" "$ISSUER/oauth/token"
The answer is 401 with WWW-Authenticate: Basic realm="OAuth token" and {"error":"invalid_client",...}.
Why it matters: a client ID tells the server which registration is involved. Knowing it, even while holding a valid code, is not proof that the request came from the printer.
Send the same exchange with the printer's credentials: replace
-d "client_id=$CLIENT_ID"with-u "$CLIENT_ID:$CLIENT_SECRET". The answer is200with anaccess_token. The refused attempt in step 3 did not use up the code, because client authentication is checked first.
Now the public app. Start its own loopback listener in a second terminal with
btl-lab callback --port 8765, then start an authorization without PKCE.
eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=$(enc http://127.0.0.1:8765/callback)&scope=photos.read&state=$STATE"
The browser returns to the listener at once with error=invalid_request, error_description=This client must send a PKCE code_challenge., state and iss.
Why it matters: a public client has no secret to prove itself with, so the server requires a fresh proof for each attempt instead.
Repeat with a challenge, sign in as Ava, approve, and exchange with no secret at all.
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$APP_ID&redirect_uri=$(enc http://127.0.0.1:8765/callback)&scope=photos.read&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
read -r CODE # from the listener's output
curl -s -d grant_type=authorization_code -d "code=$CODE" --data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" \
-d "code_verifier=$VERIFIER" -d "client_id=$APP_ID" "$ISSUER/oauth/token" | jq
The answer is 200 with an access token. The registration says http://127.0.0.1/callback with no port, and the request used port 8765: loopback addresses match on any port.
Why it matters: PKCE links the exchange to the app instance that started the attempt, so someone who copies the code without the verifier cannot redeem it. It does not identify the application the way the printer's secret does.
Break it
Give the public app an invented secret: add
-u "$APP_ID:made-up-secret-made-up-secret-12345"to a fresh step 6 exchange. The answer is401 invalid_client. Inventing a secret does not make a public client confidential.Start a printer authorization without
code_challenge. It is refused withpkce_requiredtoo, because this confidential client's PKCE policy is Required.
Check your work
Press Check my progress. In tenant Audit you can also find two tenant.oauth.clients.create events and tenant.oauth.credentials.rotate. The printer's successful oauth.token has actor lab-printer, an authenticated client; the public app's has no client actor, because a public client's client_id is only a claim.
Cleanup
Keep both clients and the printer's secret. The authorization code labs use them.