OAUTH 2.0 · LAB
Sign a desktop app in through the system browser with PKCE
Run the printer app's public client flow in your real browser, redeem the code with only a verifier, stop silent approval, keep the refresh token in system storage and sign out by revoking.
Partly readyUses your lab tenant
The lesson
Builds on: Public and confidential clients.
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.
- G54 Private-use URI scheme redirects for native apps (RFC 8252)
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.
Redeem the app's code with only a verifier
Recorded as
oauth.tokensucceeded forlab-printer-appabout[email protected].See a code refused without its verifier
Recorded as
oauth.tokenrejected (pkce_failed) forlab-printer-app.Make the public client ask on every request
Recorded as
tenant.oauth.clients.updatesucceeded.Sign the app out by revoking its refresh token
Recorded as
oauth.revokesucceeded (refresh_token_found) forlab-printer-app.
Setup
Choose Lab Photos as the lab tenant and press Start.
In Clients, confirm
lab-printer-appis Public with PKCE required, the authorization code and refresh token grants, and the loopback redirect URIhttp://127.0.0.1:8765/callback. Flow policy must allow the refresh token grant. Note its Consent interaction setting.Set
ISSUERandCLIENT_IDtolab-printer-app's ID. There is no secret to load. Use theauthorizehelper from Present an access token correctly, and an exchange that names the client instead of authenticating:
app_exchange() { # $1 = verifier to send; omit to send none
read -r CODE
RESP=$(curl -s "$ISSUER/oauth/token" -d grant_type=authorization_code -d "client_id=$CLIENT_ID" --data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=http://127.0.0.1:8765/callback" ${1:+-d "code_verifier=$1"})
TOKEN=$(jq -r '.access_token // empty' <<<"$RESP"); REFRESH=$(jq -r '.refresh_token // empty' <<<"$RESP")
jq 'del(.access_token, .refresh_token, .id_token)' <<<"$RESP"
}
Run
btl-lab callbackin a second terminal before each sign-in. It plays the app's loopback listener.
Walkthrough
Sign in through the system browser. Run
authorize "openid photos.read offline_access"and open the URL in your everyday browser, where you may already be signed in to the tenant. Check the address bar shows your tenant's host. Approve.
Why it matters: the system browser shows a real address bar and shares its existing session, and the app can neither read the password nor press Approve. An embedded web view would give the app all of that.
Redeem the code with only the client ID and the verifier:
app_exchange "$VERIFIER". You get an access token and a refresh token.
Why it matters: there is no Authorization header because the app has no credential. The verifier is the only proof that this copy of the app started the attempt.
Redeem a second code without the verifier. Start a new attempt with
authorize, approve, and runapp_exchangewith no argument. Returnsinvalid_grant, Audit reasonpkce_failed.
Why it matters: a code delivered to another app on the same device, for example one that registered the same private-use scheme, is useless without the verifier.
Stop silent approval. Start another attempt in the same browser. With Consent interaction set to ask once per set of scopes, no consent screen appears. In Clients, set
lab-printer-app's Consent interaction to ask on every request and save, then start another attempt: the consent screen appears.
Why it matters: any software can claim a public client's ID, so the provider should not approve silently just because Ava approved the app before, unless it can confirm the app's identity.
Try a private-use scheme. In Clients, add the redirect URI
com.example.labprinter:/oauth/callbacktolab-printer-appand save. The tenant refuses it.
Note: RFC 8252 allows reverse-domain private-use schemes for native apps; this tenant does not accept them yet (G54). Claimed HTTPS redirects need the operating system to verify the app's association with the domain, which is outside the tenant.
Keep the refresh token in the system's secret store rather than a file. Use whichever your system has:
macOS:
security add-generic-password -a lab-printer-app -s btl-lab -w "$REFRESH"Linux with a keyring:
printf %s "$REFRESH" | secret-tool store --label=btl-lab service btl-lab account lab-printer-appWindows: store it with Credential Manager in PowerShell, or skip this step and keep it in the shell only.
Why it matters: app files are copied into backups and readable by anything that reaches the app's storage. A system keychain encrypts the secret and can keep it to this device.
Refresh as a public client:
curl -s "$ISSUER/oauth/token" -d grant_type=refresh_token -d "client_id=$CLIENT_ID" --data-urlencode "refresh_token=$REFRESH". A new refresh token comes back; save it over the old one in the secret store at once.
Why it matters: the provider rotates a public client's refresh tokens because it cannot authenticate the client. The app saves each replacement as soon as it arrives.
Sign out of the app: revoke the current refresh token, then delete it from storage.
curl -s -w 'HTTP %{http_code}\n' "$ISSUER/oauth/revoke" -d "client_id=$CLIENT_ID" --data-urlencode "token=$REFRESH" -d token_type_hint=refresh_token
Returns 200. Then remove the stored copy (security delete-generic-password -a lab-printer-app -s btl-lab, or secret-tool clear service btl-lab account lab-printer-app).
Why it matters: a public client may revoke its own tokens by naming itself. Deleting the app without signing out would leave the refresh token valid at the provider.
Break it
Step 3 is the failure case: a code redeemed without its verifier. Nothing in the tenant is weakened.
Check your work
Press Check my progress. The checks look for, in order: the app's code exchange for Ava, the pkce_failed refusal, the tenant.oauth.clients.update that changed consent, and oauth.revoke with refresh_token_found for lab-printer-app.
Cleanup
Set
lab-printer-app's Consent interaction to the value you noted in Setup if you prefer it, though asking on every request is the safer choice for a public client.Confirm the stored refresh token is deleted, and run
unset TOKEN REFRESH RESP CODE.
Missing infrastructure
G54 Private-use URI scheme redirects. The tenant rejects
com.example.labprinter:/oauth/callback. Once supported, the lab adds a step registering that scheme, handing the response to a small desktop handler that receives it, and showing why the verifier matters when another app can claim the same scheme. Claimed HTTPS redirects stay out of scope, because they depend on the operating system verifying the app's domain association.