OAUTH 2.0 · LAB
Read the issuer in every authorization response from two providers
Register the collage app with Lab Photos and Lab Mail, then see each provider name itself with iss in successes and errors, and match it to metadata and tokens.
Partly readyUses both lab tenants
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.
- G66 Second lab tenant for every learner: additional tenants need a paid subscription or a BTL grant, so labs that use Lab Mail cannot be completed by an ordinary learner yet
Needs a second tenant. This lab also uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so you may not be able to do the Lab Mail steps yet (gap G66).
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.
Receive a code from Lab Photos that names its issuer
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collage.Redeem it at the token endpoint from Lab Photos metadata
Recorded as
oauth.tokensucceeded forlab-collage.Deny a request and read the issuer on the error
Recorded as
oauth.authorizerejected (access_denied) forlab-collage.
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 starts the issuer track. The collage app is registered with two of your own tenants, so its callback receives responses from two authorization servers, exactly like the lesson's printer.
Choose Lab Photos as the lab tenant and copy its issuer from Overview. If you do not have a second tenant yet, create Lab Mail from the tenant switcher and copy its issuer too. Both are your tenants; nothing in this track touches anyone else's.
export ISSUER='https://tenant-<lab-photos-id>.beyondthelogin.dev'
export ISSUER2='https://tenant-<lab-mail-id>.beyondthelogin.dev'
In Lab Photos, open Users and set a password for
[email protected]. If Ava does not exist, create her (or reset the lab tenant with the Lab Photos preset when you are starting from nothing).In Lab Mail, create a user
[email protected]with a different password. It is a separate account in a separate directory: the same email address at another issuer is another person as far as each provider is concerned.In each tenant, open OAuth > Flow policy and confirm
authorization_codeis allowed.In Lab Photos, open OAuth > Clients and create
lab-collagewith the Web preset: confidential, redirect URIhttp://127.0.0.1:8765/callback, PKCE required, grantauthorization_code, scopeopenid, consent mode Always (so you can choose Deny). In Lab Mail, createlab-mail-collagewith the same settings. If the OIDC track already created them, add any missing setting.Store each client ID and the secret shown once. The secrets go into the shell only, never into a page or file:
export CLIENT_ID='<lab-collage client id>'; read -rs CLIENT_SECRET; export CLIENT_SECRET
export MAIL_CLIENT_ID='<lab-mail-collage client id>'; read -rs MAIL_CLIENT_SECRET; export MAIL_CLIENT_SECRET
btl-lab env
Press Start on this page with Lab Photos selected.
Walkthrough
Read what Lab Photos says about itself.
GET$ISSUER/.well-known/oauth-authorization-server
Open in console
GET $ISSUER/.well-known/oauth-authorization-serverThe response includes these members (shortened):
{
"issuer": "https://tenant-<lab-photos-id>.beyondthelogin.dev",
"authorization_endpoint": "https://tenant-<lab-photos-id>.beyondthelogin.dev/oauth/authorize",
"token_endpoint": "https://tenant-<lab-photos-id>.beyondthelogin.dev/oauth/token",
"jwks_uri": "https://tenant-<lab-photos-id>.beyondthelogin.dev/oauth/jwks",
"authorization_response_iss_parameter_supported": true
}
Now read Lab Mail's document and compare the two:
GET$ISSUER2/.well-known/oauth-authorization-server
Open in console
GET $ISSUER2/.well-known/oauth-authorization-serverWhy it matters: an issuer identifier is an HTTPS URL with no query or fragment that names a whole configuration: an authorization endpoint, a token endpoint and a key set. authorization_response_iss_parameter_supported: true is each server's promise that every authorization response will carry iss.
Start an attempt at Lab Photos and remember which server you asked. In a second terminal, start the loopback listener; it waits for one response, prints
code,state,issand any error, then exits. Run it again before each attempt in this lab.
btl-lab callback
In your main terminal:
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
EXPECTED_ISSUER="$ISSUER"
AUTHZ=$(curl -s "$EXPECTED_ISSUER/.well-known/oauth-authorization-server" | jq -r .authorization_endpoint)
echo "$AUTHZ?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Open the printed URL, sign in as Ava with her Lab Photos password and approve. The address bar ends with ?code=...&state=...&iss=https%3A%2F%2Ftenant-.... The value is form-encoded because it sits in a URL; the listener prints it decoded once.
Why it matters: the client records the issuer it expected when the attempt begins. Plain OAuth 2.0 responses carry only code and state, so without iss that record would be the only clue to which server answered.
Copy the listener's values into the shell and compare.
read -rs CODE; read -r RESP_STATE; read -r RESP_ISS
[ "$RESP_ISS" = "$EXPECTED_ISSUER" ] && [ "$RESP_STATE" = "$STATE" ] && echo "expected server, this attempt"
Why it matters: the decoded iss is the issuer identifier, character for character. RFC 9207 requires it to be identical to the metadata issuer.
Redeem the code at the token endpoint of the server you expected, read from its metadata, never from anything in the response.
TOKEN_ENDPOINT=$(curl -s "$EXPECTED_ISSUER/.well-known/oauth-authorization-server" | jq -r .token_endpoint)
RESP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$TOKEN_ENDPOINT" -d grant_type=authorization_code --data-urlencode "code=$CODE" \
--data-urlencode redirect_uri=http://127.0.0.1:8765/callback --data-urlencode "code_verifier=$VERIFIER")
TOKEN=$(jq -r .access_token <<<"$RESP"); ID_TOKEN=$(jq -r .id_token <<<"$RESP"); unset CODE RESP
btl-lab decode "$TOKEN"
btl-lab decode "$ID_TOKEN"
Both payloads show iss with the same string you compared in step 3. Note Ava's sub in the ID token.
Why it matters: one identifier appears as the metadata issuer, the response iss and the token iss. Knowing which issuer answered tells the client which token endpoint the code belongs at.
An error names its sender too. Run the listener again, repeat step 2 with fresh PKCE and state values, and choose Deny on the consent screen. The listener prints
error=access_denied, yourstateandiss.
read -r RESP_STATE; read -r RESP_ISS
[ "$RESP_ISS" = "$EXPECTED_ISSUER" ] && echo "Lab Photos sent this refusal"
Why it matters: an error is a message from a server. The client checks which server sent it before telling the user that this server refused anything.
Lab Mail names itself. Run the listener, then start an attempt at Lab Mail with its own client:
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
EXPECTED_ISSUER="$ISSUER2"
AUTHZ=$(curl -s "$EXPECTED_ISSUER/.well-known/oauth-authorization-server" | jq -r .authorization_endpoint)
echo "$AUTHZ?response_type=code&client_id=$MAIL_CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid&state=$STATE&code_challenge=$CHALLENGE&code_challenge_method=S256"
Sign in as Ava with her Lab Mail password and approve. Read the values as in step 3: iss is now $ISSUER2. Redeem as in step 4 with MAIL_CLIENT_ID and MAIL_CLIENT_SECRET, and decode the ID token: its sub differs from the Lab Photos sub.
Why it matters: two responses that plain OAuth made indistinguishable now name their senders. The same email at two issuers is two accounts, which is why a client identifies a user by iss and sub together.
Break it
A contained change on Lab Mail only: open OAuth > Flow policy, clear
authorization_codeand save. Then read the metadata again:
GET$ISSUER2/.well-known/oauth-authorization-server
Open in console
GET $ISSUER2/.well-known/oauth-authorization-serverauthorization_response_iss_parameter_supported is now false, and code_challenge_methods_supported and response_types_supported are empty: with no authorization response allowed, Lab Mail no longer promises iss. A new Lab Mail attempt returns error=unauthorized_client to the listener, and the refusal still carries iss, because this server names itself on every response it sends.
Restore: re-enable authorization_code in Lab Mail's Flow policy and save. The metadata returns to true and ["S256"]. Discovery is served with Cache-Control: public, max-age=300, so a caching client may see the old document for up to five minutes.
Why it matters: the flag describes what the server promises under its current configuration, and a client that reads it must refresh it.
Check your work
Press Check my progress. The checks read Lab Photos:
oauth.authorizesucceeded withcode_issuedforlab-collage.oauth.tokensucceeded forlab-collage.oauth.authorizerejected withaccess_deniedforlab-collage.
In Lab Mail's Audit you will also find the code_issued event for lab-mail-collage and two tenant.oauth.policy.update events from Break it.
Cleanup
Keep both tenants, both clients, Ava's two accounts and the shell variables for the rest of the track. Confirm that Lab Mail's Flow policy allows authorization_code again. Run unset TOKEN ID_TOKEN when you finish.
Missing infrastructure
G66 Second lab tenant: this lab uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so an ordinary learner can do only the Lab Photos steps until every learner can have a second lab tenant.