OAUTH 2.0 · LAB
See what a browser-carried authorization request exposes
Read, edit and hand-build authorization requests for lab-printer, then use Audit to show that the tenant first authenticates the client only at the token request.
Partly readyUses your lab tenant
The lesson
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.
- G11 PAR
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.
Edit the request to ask for a scope the client may not request
Recorded as
oauth.authorizerejected (invalid_scope) forlab-printer.Approve a request that no client application built
Recorded as
oauth.authorizesucceeded (code_issued) forlab-printer.Redeem the code, the first time the client authenticates
Recorded as
oauth.tokensucceeded forlab-printer.Redeem a code with the wrong secret after Ava already approved
Recorded as
oauth.tokenrejected (invalid_client) forlab-printer.
Setup
In the tenant portal, open OAuth > Scopes and confirm that
photos.read,prints.createandphotos.deleteexist. If they do not, reset the lab tenant with the Lab Photos preset.Open OAuth > Clients and find
lab-printer. If it does not exist yet, create it with the Web application preset: confidential, grantsauthorization_codeandrefresh_token, PKCE required, redirect URIhttp://127.0.0.1:8765/callback.On
lab-printer, set the consent mode to always, turn on Restrict scopes, and allowopenid,profile,photos.readandprints.create. Leavephotos.deleteoff. Save.Make sure Ava can sign in: in Users, set a password for
[email protected]if she has none.In a terminal, set the shell variables. Read the secret without echoing it.
export ISSUER=https://tenant-<id>.beyondthelogin.dev # from the tenant Overview
export CLIENT_ID=<lab-printer client_id>
read -rs CLIENT_SECRET; export CLIENT_SECRET
export REDIRECT=http://127.0.0.1:8765/callback
urlenc() { jq -rn --arg v "$1" '$v|@uri'; }
In a second terminal, start the loopback listener with
btl-lab callback. It printscode,state,issor an error, then exits; start it again before each attempt. If you have no terminal handy, addhttps://beyondthelogin.dev/lab/callback/as a redirect URI onlab-printerand use that instead.
Walkthrough
Build an ordinary authorization request and measure it.
btl-lab pkce # export the printed VERIFIER and CHALLENGE
btl-lab state # export the printed STATE
URL="$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(urlenc $REDIRECT)&scope=$(urlenc 'openid photos.read')&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
echo "$URL"; printf %s "$URL" | wc -c
Open the URL in a private window and sign in as Ava. Stop at the consent screen, which names the printer and photos.read. Now open the window's history: the whole request, scope, return address, state and challenge, is stored there.
Why it matters: this is "A request in plain sight". Every parameter travels in the address bar and is recorded wherever URLs are recorded.
In the address bar, change
scopetoopenid photos.read prints.create(encode the spaces as%20) and press Enter. The consent screen now asks for both scopes.
Why it matters: this is "A request anyone can change". Both scopes are registered for the client, so the tenant cannot tell an edited request from one the printer built.
Edit the address again so
scopealso containsphotos.delete, which the client may not request. The listener printserror=invalid_scopewith yourstateandiss.
Why it matters: registration checks still limit an edit to what the client may request, exactly as the lesson says. Within those limits, the tenant cannot know which values the client chose.
Change
redirect_urito an address that is not registered, such ashttps://printer.example/oauth/callback. The tenant shows its own error page and sends nothing to the listener.
Why it matters: the tenant may not redirect to an address it cannot trust, so a mistake in the return address is invisible to the client.
Build a second request by hand in a new shell, with no client application involved: run
btl-lab pkceandbtl-lab stateagain, rebuild the URL, open it and approve as Ava. The listener prints a validcode.
Why it matters: the consent screen showed the printer's name for a request no printer software built. The tenant cannot know who wrote a browser-carried request.
Redeem that code. This is the first point in the flow where the client authenticates.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code \
-d code=<code> --data-urlencode redirect_uri=$REDIRECT -d code_verifier=$VERIFIER | jq '{token_type, scope, expires_in}'
Why it matters: this is "Checks that come too late". Sign-in and consent happened before the tenant knew which client was really asking.
Open Audit in the tenant portal and filter to
oauth.authorizeandoauth.tokenforlab-printer. Put the events from steps 5 and 6 in order:request_started, then the sign-in and consent, thencode_issuedwith no client credential involved, and only thenoauth.tokensucceeded with the client as actor.
Why it matters: the tenant's own evidence shows the person was asked to approve before any client authentication took place.
Planned walkthrough
Once the tenant has a PAR endpoint (G11), finish the lab with the fix the lesson describes in "Sending the request directly".
Push the step 1 parameters from the back end, authenticated like a token request.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/par" \
-d response_type=code -d client_id=$CLIENT_ID --data-urlencode redirect_uri=$REDIRECT \
--data-urlencode "scope=openid photos.read" -d state=$STATE \
-d code_challenge=$CHALLENGE -d code_challenge_method=S256 | jq .
Expect 201 with a request_uri and expires_in.
Open
$ISSUER/oauth/authorize?client_id=$CLIENT_ID&request_uri=<url-encoded request_uri>. The address bar holds no scope to edit.Push again with a wrong secret. Expect
401 invalid_clientbefore Ava sees any page. Compare this with Break it below.
The next three labs carry the full PAR walkthrough.
Break it
Run steps 1 and 5 again to get a fresh code, then redeem it with a wrong secret.
read -rs WRONG_SECRET
curl -s -u "$CLIENT_ID:$WRONG_SECRET" "$ISSUER/oauth/token" -d grant_type=authorization_code \
-d code=<code> --data-urlencode redirect_uri=$REDIRECT -d code_verifier=$VERIFIER | jq .
unset WRONG_SECRET
The answer is 401 {"error":"invalid_client",...}, and Audit records oauth.token rejected invalid_client, after Ava had already signed in and approved. This is the late failure that PAR moves to the front.
Check your work
Press Check my progress. The checks look for, in order:
oauth.authorizerejectedinvalid_scopeforlab-printer(step 3)oauth.authorizesucceededcode_issued(step 5)oauth.tokensucceeded forlab-printer(step 6)oauth.tokenrejectedinvalid_clientforlab-printer(Break it)
Cleanup
Revoke the access token from step 6.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$ISSUER/oauth/revoke" -d token=<access_token>
Clear the private window's history and stop the listener.
Set
lab-printer's consent mode back to remember if you prefer fewer consent screens in later labs.
Missing infrastructure
G11: there is no PAR endpoint yet.
POST $ISSUER/oauth/paranswers501and discovery has nopushed_authorization_request_endpoint. Once it exists, the Planned walkthrough runs as written: the scope edit has nothing to edit, and a wrong secret fails at the push before anyone sees a page.