IDENTITY FUNDAMENTALS · LAB
Read real requests, redirects and errors from your tenant
Install the lab toolkit, register the photo printer, then read real URLs, headers, status codes, redirects and JSON as your tenant sends the browser to sign in and back to the application.
ReadyUses your lab tenant
The lesson
Builds on: Proving control of an account.
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 photo printer as a client
Recorded as
tenant.oauth.clients.createsucceeded.Ask for a silent sign-in with no session
Recorded as
oauth.authorizerejected (login_required) forlab-printer.Sign in as Ava and get a code for the printer
Recorded as
oauth.authorizesucceeded (code_issued) forlab-printerabout[email protected].Exchange the code directly at the token endpoint
Recorded as
oauth.tokensucceeded forlab-printer.See a request without PKCE sent back with an error
Recorded as
oauth.authorizerejected (pkce_required) forlab-printer.
Request console
Requests in this lab can be sent from this page to your tenant: open one and choose Send. Fill in the values below first. They stay in this page's memory and are gone when you leave; secrets are never stored or sent anywhere except the request you send.
Setup
This lab installs the lab toolkit that later labs use. You need bash (Git Bash on Windows is fine), curl, jq and Node.js 18 or later.
On this lab page choose Lab Photos and press Start.
Download the toolkit to your home directory and check it before you run it. Compare the printed SHA-256 value with the one shown on the toolkit page. If they differ, delete the file and do not run it.
cd ~
curl -fsSLO https://beyondthelogin.dev/lab/toolkit/btl-lab.mjs
sha256sum btl-lab.mjs # on macOS: shasum -a 256 btl-lab.mjs
alias btl-lab="node ~/btl-lab.mjs"
btl-lab env
Note: the toolkit only reads, validates, relays, or acts as your own client. It never creates, signs or alters a token. Add the alias to your shell profile so later labs can use btl-lab directly.
Clients > Create client with the web application preset:
name
lab-printer, confidentialredirect URIs
http://127.0.0.1:8765/callbackandhttps://beyondthelogin.dev/lab/callback/grant: authorization code, PKCE Required (the default)
Keep the client's details in the shell. The secret is shown once.
ISSUER=https://tenant-<id>.beyondthelogin.dev
CLIENT_ID=<lab-printer client ID>
read -rs CLIENT_SECRET
REDIRECT=http://127.0.0.1:8765/callback
authz() { echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(node -p 'encodeURIComponent(process.argv[1])' "$REDIRECT")&scope=$1&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"; }
redeem() { curl -s -u "$CLIENT_ID:$CLIENT_SECRET" -d grant_type=authorization_code --data-urlencode "code=$CODE" --data-urlencode "redirect_uri=$REDIRECT" -d code_verifier="$VERIFIER" "$ISSUER/oauth/token"; }
fresh() { eval "$(btl-lab pkce)"; eval "$(btl-lab state)"; }
To receive codes, run btl-lab callback in a second terminal before each browser sign-in; it listens on the loopback address and prints code, state and iss. Without a terminal handy, set REDIRECT=https://beyondthelogin.dev/lab/callback/ instead and read them on the hosted callback page, which never exchanges or stores them.
Walkthrough
Read a JSON API response. Open this request in the console, or run it with
curl -i:
GET$ISSUER/.well-known/openid-configuration
Open in console
GET $ISSUER/.well-known/openid-configuration HTTP/1.1
Accept: application/jsonExpected (abbreviated): 200, content-type: application/json, an x-request-id header, and a body with issuer, authorization_endpoint, token_endpoint and jwks_uri. Then run btl-lab discover "$ISSUER" for a summary of the same document and the key set it points to.
Why it matters: these are the parts the lesson named in its album request: scheme, host, path, method, headers, status and a JSON body.
Ask for a private page without signing in:
curl -i "$ISSUER/account"
curl -sL -o /dev/null -w '%{http_code} %{url_effective}\n' "$ISSUER/account"
Expected: 303 with location: /login?continue=%2Faccount, and with -L curl follows it to the sign-in page (200, HTML).
Why it matters: this is the lesson's private album and its redirect to a sign-in page, on a real server. The Location header made the client send a new request.
Call an API without credentials:
GET$ISSUER/oidc/userinfo
Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Accept: application/jsonExpected: 401 with www-authenticate: Bearer.
Why it matters: a program calling an API gets an error it can act on, not a sign-in page.
Ask the authorization server to sign in silently on behalf of
lab-printer:
fresh; curl -si "$(authz openid)&prompt=none" | grep -i '^location'
Expected: 303 with a location back to your REDIRECT carrying error=login_required, your state and iss.
Why it matters: the identity service sends the browser back to the application's registered address. state comes back unchanged, so the application can connect this response to the request it sent.
The same request without
prompt=none:
fresh; curl -si "$(authz openid)" | grep -iE '^(HTTP|content-type|set-cookie)'
Expected: 200, content-type: text/html, and set-cookie: __Host-btl-oauth-tx=...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=600.
Why it matters: the server keeps this sign-in attempt in a short-lived cookie, so that the next request, the form post, can be connected to it. The next lab follows that idea into sessions.
Now in a browser. Run
fresh; authz openidand copy the printed URL. Open DevTools > Network with "Preserve log" ticked, paste the URL, and sign in as Ava with her password and authenticator code. Approve the consent screen. Network shows the chain: the authorize page, the sign-in post, a303to/login/verify, then a303to your redirect URI withcode,stateandiss.
Why it matters: each Location header made the browser send a new request, and the code reached the application through the browser.
Exchange the code directly, without the browser:
CODE=<code from the callback>
redeem | jq '{token_type, expires_in, scope, has_id_token: (.id_token != null)}'
Why it matters: redirects travel through the browser, but this call goes straight from the application to the authorization server. The OAuth lessons build on that split between front channel and back channel.
Open Audit and find
oauth.authorizewith reasoncode_issuedandoauth.tokensucceeded forlab-printer. Open an event and compare its request ID with anx-request-idheader you saw.
Why it matters: request IDs connect what a client saw with what the server recorded about the same request.
Break it
Run
freshand takeauthz openid, but replace theredirect_urivalue withhttps%3A%2F%2Fevil.example%2Fcallback. Expected: a400HTML error page and noLocationheader.
Why it matters: a server that redirected anywhere it was asked would hand codes to whoever named the address.
Take a fresh
authz openidURL and removecode_challengeandcode_challenge_method. Expected: a303back to the registered redirect URI witherror=invalid_request, becauselab-printerrequires PKCE. Audit shows reasonpkce_required.
Check your work
Press Check my progress. It looks for:
tenant.oauth.clients.createsucceeded (lab-printer).oauth.authorizerejected with reasonlogin_requiredforlab-printer.oauth.authorizesucceeded with reasoncode_issuedforlab-printerand Ava.oauth.tokensucceeded forlab-printer.oauth.authorizerejected with reasonpkce_requiredforlab-printer.
Cleanup
Keep
lab-printer, its secret and the toolkit. Later labs use them.Close the private window. Nothing else changed.