OAUTH 2.0 · LAB
Play the printer's backend and keep every token off the browser
Run the confidential web flow with your shell as the backend, keep an encrypted connection record, and show that signing out ends a session but not the connection.
ReadyUses your lab tenant
The lesson
Builds on: Refresh tokens and rotation.
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.
Connect Ava's photos to the printer backend
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Sign Ava out of the tenant
Recorded as
account.sign_outsucceeded (signed_out) about[email protected].Run the December job after sign-out
Recorded as
oauth.tokensucceeded forlab-printerabout[email protected].Disconnect by revoking the refresh token
Recorded as
oauth.revokesucceeded (refresh_token_found) forlab-printer.Lock Ava's account
Recorded as
tenant.users.locksucceeded.See the job's refresh refused after the lock
Recorded as
oauth.tokenrejected (refresh_revoked) forlab-printer.
Setup
Choose Lab Photos as the lab tenant and press Start.
In Clients, open
lab-printerand compare it with the Web application preset: Confidential, authorization code and refresh token, PKCE for confidential clients Required. Flow policy must allow the refresh token grant.Load
ISSUER,CLIENT_IDandCLIENT_SECRET(withread -rs) forlab-printer, and theauthorizehelper from Present an access token correctly.Create a key that stands in for the printer's key management service. It lives only in this shell, never in a file:
RECORD_KEY=$(openssl rand -hex 32); export RECORD_KEY
seal() { openssl enc -aes-256-cbc -pbkdf2 -a -A -pass env:RECORD_KEY; }
unseal() { openssl enc -d -aes-256-cbc -pbkdf2 -a -A -pass env:RECORD_KEY; }
mkdir -p -m 700 ~/lab-printer-connections
Walkthrough
Watch what the browser carries. Open developer tools, Network, with Preserve log on. Run
btl-lab callbackin a second terminal, thenauthorize "openid photos.read offline_access", open the URL, sign in as Ava and approve. The redirect chain shows the authorization request going out andcode,stateandisscoming back. No response to the browser contains a token.
Why it matters: the browser carries the request out and the code back. Every token arrives in a direct response to the backend.
Look at what the browser does hold. In developer tools, Application, Cookies for your tenant host: the session cookie is
HttpOnlyandSecure, with aSameSiteattribute. Rundocument.cookiein the console on the tenant page: the session cookie is not listed.
Why it matters: a script injected into the page could misuse the session while the page is open, but it finds no token to carry away and replay against an API from somewhere else.
Exchange the code in the shell, the backend, and save a connection record tied to the printer account. The refresh token is sealed with the key; nothing is printed.
read -r CODE
RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code --data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" -d "code_verifier=$VERIFIER")
jq -n --arg iss "$ISSUER" --arg cid "$CLIENT_ID" --arg scope "$(jq -r .scope <<<"$RESP")" --arg rt "$(jq -r .refresh_token <<<"$RESP" | seal)" \
'{account: "ava", issuer: $iss, client_id: $cid, granted_scope: $scope, refresh_token: $rt, status: "active"}' > ~/lab-printer-connections/ava.json
chmod 600 ~/lab-printer-connections/ava.json; unset RESP CODE; jq 'del(.refresh_token)' ~/lab-printer-connections/ava.json
Why it matters: the record holds the scope as the token response reported it, which may be narrower than requested. Unlike an authorization server, the printer cannot store only a hash, because it must send the original value back, so it encrypts it with a key held elsewhere.
Sign out. In the browser, open
$ISSUER/accountand select Sign out.
Why it matters: this ends Ava's session at the tenant, which stands in for her printer session here. The connection is a separate thing.
Run the December job with no browser. It loads the record by the printer account it works for, refreshes, saves the replacement before using the new access token, then reads the profile:
REC=~/lab-printer-connections/ava.json
RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=refresh_token \
--data-urlencode "refresh_token=$(jq -r .refresh_token "$REC" | unseal)")
jq --arg rt "$(jq -r .refresh_token <<<"$RESP" | seal)" '.refresh_token = $rt' "$REC" > "$REC.new" && mv "$REC.new" "$REC"
curl -s -o /dev/null -w 'UserInfo %{http_code}\n' "$ISSUER/oidc/userinfo" -H "Authorization: Bearer $(jq -r .access_token <<<"$RESP")"; unset RESP
Returns UserInfo 200, after Ava signed out.
Why it matters: the connection outlives the session that created it. Only one job refreshes a connection at a time, and the job finds the connection through the account it is working for, never from an identifier carried in a queue message.
Disconnect. Revoke the refresh token from the record, and delete the record only after the server answers
200:
curl -s -o /dev/null -w '%{http_code}\n' -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" \
--data-urlencode "token=$(jq -r .refresh_token "$REC" | unseal)" -d token_type_hint=refresh_token
rm "$REC"
Why it matters: Disconnect ends the grant; signing out did not. Neither touched the other.
Reconnect (steps 1 and 3), then lock Ava in User Management. Run the job from step 5: the refresh returns
invalid_grant. Instead of retrying, mark the record:
jq '.status = "needs_attention"' "$REC" > "$REC.new" && mv "$REC.new" "$REC"; jq '.status' "$REC"
Why it matters: when the provider ends access, the job learns it at its next refresh. It should notify the person rather than loop.
Restore: unlock Ava in User Management.
Break it
Exchange a code without the client secret, as a public client would, with -d client_id=$CLIENT_ID instead of -u. Returns 401 invalid_client, Audit reason unsupported_auth_method. A confidential client's code is useless to anyone who lacks its credentials.
Check your work
Press Check my progress. The checks look for, in order: the code exchange for Ava, account.sign_out for Ava, a refresh after the sign-out, oauth.revoke with refresh_token_found, tenant.users.lock, and the job's refresh refused with refresh_revoked.
Cleanup
Confirm Ava is unlocked.
Delete the connection records:
rm -rf ~/lab-printer-connections. Rununset RECORD_KEY REC.