Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

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.

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.

  1. Receive a code from Lab Photos that names its issuer

    Recorded as oauth.authorize succeeded (code_issued) for lab-collage.

  2. Redeem it at the token endpoint from Lab Photos metadata

    Recorded as oauth.token succeeded for lab-collage.

  3. Deny a request and read the issuer on the error

    Recorded as oauth.authorize rejected (access_denied) for lab-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.

  1. 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'
  1. 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).

  2. 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.

  3. In each tenant, open OAuth > Flow policy and confirm authorization_code is allowed.

  4. In Lab Photos, open OAuth > Clients and create lab-collage with the Web preset: confidential, redirect URI http://127.0.0.1:8765/callback, PKCE required, grant authorization_code, scope openid, consent mode Always (so you can choose Deny). In Lab Mail, create lab-mail-collage with the same settings. If the OIDC track already created them, add any missing setting.

  5. 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
  1. Press Start on this page with Lab Photos selected.

Walkthrough

  1. Read what Lab Photos says about itself.

GET$ISSUER/.well-known/oauth-authorization-server Open in console
GET $ISSUER/.well-known/oauth-authorization-server

The 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-server

Why 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.

  1. 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, iss and 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.

  1. 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.

  1. 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.

  1. 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, your state and iss.

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.

  1. 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

  1. A contained change on Lab Mail only: open OAuth > Flow policy, clear authorization_code and save. Then read the metadata again:

GET$ISSUER2/.well-known/oauth-authorization-server Open in console
GET $ISSUER2/.well-known/oauth-authorization-server

authorization_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.authorize succeeded with code_issued for lab-collage.

  • oauth.token succeeded for lab-collage.

  • oauth.authorize rejected with access_denied for lab-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.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab