OAUTH 2.0 · LAB
Turn on the implicit grant, read the fragment, and turn it off
Allow the implicit grant for one temporary client, receive a token in the URL fragment, see where it ends up and what binds it to nothing, then replace it with the code flow and switch it off.
ReadyUses your lab tenant
The lesson
Builds on: 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.
Sign in to start this lab and check your progress. Log in or create an account.
Allow the implicit grant in Flow policy
Recorded as
tenant.oauth.policy.updatesucceeded.A token arrives in the redirect
Recorded as
oauth.authorizesucceeded (tokens_issued) forlab-tmp-editor.A token in the query string is refused
Recorded as
oauth.authorizerejected (unsupported_response_mode) forlab-tmp-editor.The same client uses the code flow with PKCE
Recorded as
oauth.tokensucceeded forlab-tmp-editor.Switch the implicit grant off again
Recorded as
tenant.oauth.policy.updatesucceeded.The implicit request is refused once it is off
Recorded as
oauth.authorizerejected (unauthorized_client) forlab-tmp-editor.
Setup
Press Start on this page.
Open OAuth > Flow policy. Tick Implicit under Allowed grants,
tokenunder Allowed authorization responses andfragmentunder Allowed response modes. The page shows the RFC 9700 warning. Save.
Restore: Break it switches all three off again, and Cleanup confirms it. Nothing else in the tenant uses them.
Create a temporary client from Single-page application named
lab-tmp-editor, type Public, with redirect URIshttps://beyondthelogin.dev/lab/callback/andhttp://127.0.0.1:8765/callback. Also tick the Implicit grant, thetokenresponse and thefragmentresponse mode. Tick Enabled and save.Set
ISSUER,EDITOR_ID(thelab-tmp-editorclient ID),API_IDandAPI_SECRETforlab-photo-api, andenc.
Walkthrough
Discovery now advertises the choice:
curl -s "$ISSUER/.well-known/openid-configuration" | jq '.grant_types_supported, .response_types_supported'includesimplicitandtoken.
Send the lesson's request. There is no PKCE challenge, because no code will be redeemed.
eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=token&client_id=$EDITOR_ID&redirect_uri=$(enc https://beyondthelogin.dev/lab/callback/)&scope=photos.read&state=$STATE"
Sign in as Ava and approve. The hosted callback page shows the fragment's parameters: access_token, token_type=Bearer, expires_in, scope=photos.read, state and iss. Keep the token as TOKEN.
Why it matters: the token itself came back in the redirect. There was no token request, no client authentication and no PKCE.
Where the fragment goes. Run
btl-lab callback --port 8765in a second terminal, then repeat step 2 withredirect_uri=$(enc http://127.0.0.1:8765/callback). The browser's address bar shows#access_token=..., but the listener prints a request for/callbackwith nothing after the path.
Why it matters: browsers never send the fragment to the server. Only script running in the page can read it, which is why the hosted callback page could show it and the listener could not.
Open your browser's history. The full address from step 2, token included, is stored there.
Why it matters: a token placed in a URL goes wherever URLs go: history, any script on the callback page, and occasionally a referrer.
Open Audit. Step 2 shows
oauth.authorizesucceededtokens_issuedforlab-tmp-editor, and nooauth.tokenevent follows it.
Ask for continued access: repeat step 2 with
scope=photos.read%20offline_access. The fragment has no refresh token. An implicit client could only send Ava back to the authorization server, as the lesson's hidden iframes did.
Which client was this token issued to?
btl-lab introspect "$TOKEN"showsclient_idequal to$EDITOR_ID. Introspect a Token Decoder token for Ava as well: a differentclient_id, and both are valid tokens for Ava. A page reading a fragment cannot introspect, so it has no way to tell a token issued to it from one issued to another client.
Why it matters: nothing in the implicit response binds the token to the client that asked. That is what made access token injection possible.
Tokens never travel in the query string. Add
&response_mode=queryto step 2's address. The callback carrieserror=invalid_request, "Responses that carry tokens cannot use the query response_mode. Use fragment or form_post."
The replacement, with the same client. Run
eval "$(btl-lab pkce)"; eval "$(btl-lab state)", open a code flow request for$EDITOR_IDwithcode_challenge=$CHALLENGE&code_challenge_method=S256, approve, and exchange with only the client ID and verifier.
read -r CODE
curl -s -d grant_type=authorization_code -d "code=$CODE" --data-urlencode "redirect_uri=https://beyondthelogin.dev/lab/callback/" \
-d "code_verifier=$VERIFIER" -d "client_id=$EDITOR_ID" "$ISSUER/oauth/token" | jq '{token_type, scope}'
The token arrives in a response body, never in a URL or history. A real single-page app could make the same call from the browser, because the token endpoint accepts requests from the origins of the client's redirect URIs.
Why it matters: CORS and PKCE removed the reasons for the workaround. RFC 9700 says clients should not use the implicit grant, and RFC 10017 says browser-based clients must not.
Break it
Switch it off for the client: in OAuth > Clients > lab-tmp-editor, untick the Implicit grant, the
tokenresponse and thefragmentmode, and save. Saving revokes the editor's tokens:btl-lab introspect "$TOKEN"answers{"active": false}.Switch it off for the tenant: in Flow policy, untick Implicit,
tokenandfragment, and save.Repeat step 2. The callback carries
error=unauthorized_client, and discovery no longer listsimplicitortoken.
Check your work
Press Check my progress. In tenant Audit you can also find tenant.oauth.clients.create and tenant.oauth.clients.update for lab-tmp-editor.
Cleanup
Confirm Flow policy no longer allows Implicit,
tokenorfragment.Delete
lab-tmp-editor.Clear the callback entries from your browser history.