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

OPENID CONNECT · LAB

Call UserInfo every allowed way and refuse a response for the wrong person

Call UserInfo with GET and POST, read each refusal from WWW-Authenticate, and add the subject comparison that stops a mispaired access token from filling Ava's account with Ben's details.

Partly readyUses both lab tenants

The lesson

Builds on: Scopes and the claims parameter.

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. UserInfo answers for Ava's access token

    Recorded as oidc.userinfo succeeded (userinfo_served) for lab-collage about [email protected].

  2. UserInfo answers for Ben's access token

    Recorded as oidc.userinfo succeeded (userinfo_served) for lab-collage about [email protected].

  3. Shorten lab-collage's access token lifetime

    Recorded as tenant.oauth.managers.update succeeded.

  4. Restore the access token lifetime

    Recorded as tenant.oauth.managers.update succeeded.

  5. Revoke Ava's access token

    Recorded as oauth.revoke succeeded (access_token_found) for lab-collage.

  6. UserInfo refuses a token without openid

    Recorded as oidc.userinfo rejected (insufficient_scope) 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

  1. You need lab-collage with its lab-collage ID token and access token managers, Ava and Ben in Lab Photos, rp_discover and the redefined helpers from the provider discovery lab, and use_provider from the several providers lab. Run rp_discover "$ISSUER" and keep btl-lab callback running.

  2. Note the current lifetime of the lab-collage access token manager.

  3. In Lab Photos, press Start.

Walkthrough

  1. Sign in as Ava with signin_url 'openid%20profile%20email' and exchange, validate the ID token, and keep both tokens: AVA_IDT=$ID_TOKEN; AVA_AT=$TOKEN. Then ask the provider, at the address discovery gave you:

GET$ISSUER/oidc/userinfo Open in console
GET $ISSUER/oidc/userinfo HTTP/1.1
Authorization: Bearer $AVA_AT

The answer is 200 with sub, name, given_name, family_name, email and email_verified.

Why it matters: these claims arrive from a separate call, with no signature. HTTPS and its certificate check are your only proof of who answered.

  1. The other allowed form, and one that is not allowed:

curl -s "$USERINFO_ENDPOINT" -d "access_token=$AVA_AT" | jq .sub
curl -si "$USERINFO_ENDPOINT?access_token=not-a-token" | head -1

The POST form returns the same sub. The query-string form is refused with 400 before any token is read. Never put a real token in a URL; this one uses a placeholder on purpose.

  1. Add the subject check to your sign-in handler:

match_subject() { local ui; ui=$(curl -s "$USERINFO_ENDPOINT" -H "Authorization: Bearer $2" | jq -r .sub)
  [ "$ui" = "$(jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | @base64d | fromjson | .sub' <<<"$1")" ] && echo "copy wanted claims" \
    || jq -cn --arg i "$ISSUER" --arg c "$(openssl rand -hex 8)" '{event: "sign_in.userinfo_subject_mismatch", message: "Sign-in stopped: UserInfo described a different subject", stage: "userinfo", issuer: $i, correlation_id: $c}'; }
match_subject "$AVA_IDT" "$AVA_AT"

It prints copy wanted claims. The function reads sub from an ID token you have already validated, and the comparison is exact: no trimming and no change of case.

  1. A pairing bug, with real tokens. In a private window sign in as Ben and keep BEN_AT=$TOKEN. Now pair the wrong tokens, as a cache keyed by the wrong value would: match_subject "$AVA_IDT" "$BEN_AT". The handler prints the mismatch record and does not copy anything. The record names the stage, the issuer and a correlation ID, and holds no token, name or email address.

Why it matters: the UserInfo response describes whoever the access token belongs to. Without the comparison, Ava's account would receive Ben's name and email address.

  1. Right provider, right endpoint. Sign in through lab-mail-collage with start_attempt mail 'openid' and handle_callback, and keep its access token: MAIL_AT=$(jq -r .access_token <<<"$RESP"). Call the Lab Photos UserInfo endpoint with it: 401 with Bearer error="invalid_token". Each provider's UserInfo answers only for its own tokens, and a plain response does not name its issuer, so you call the endpoint from the configuration of the issuer whose ID token you validated. Run rp_discover "$ISSUER" again afterwards.

  1. When to call it. Note expires_in on Ava's access token and that no refresh token was issued (you did not ask for offline_access). Call UserInfo once during sign-in, copy the claims you need into account.json, and do not call it on every page. If the call failed during sign-in, the ID token would still have established who signed in.

  1. Browser clients. Ask as a browser would:

curl -si -X OPTIONS "$USERINFO_ENDPOINT" -H 'Origin: http://localhost:3000' -H 'Access-Control-Request-Method: GET' | grep -i access-control

Expect Access-Control-Allow-Origin: *. Browser clients may call UserInfo, and the bearer token is still the only credential.

Break it

  1. An expired access token. Set the lab-collage access token manager's lifetime to 60 seconds, sign in, wait two minutes, and call UserInfo with the new token. You get 401 with WWW-Authenticate: Bearer error="invalid_token".

Restore: set the lab-collage access token manager's lifetime back to the value you noted in Setup.

  1. A revoked access token. Revoke Ava's token at the address from discovery, then call UserInfo with it:

curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$REVOCATION_ENDPOINT" -d "token=$AVA_AT"
curl -si "$USERINFO_ENDPOINT" -H "Authorization: Bearer $AVA_AT" | head -3

401 with invalid_token.

  1. Not a sign-in token. Run signin_url 'photos.read', exchange, and call UserInfo with that token: 403 with Bearer error="insufficient_scope". UserInfo expects an access token issued through an OpenID Connect sign-in.

Check your work

Press Check my progress in Lab Photos. It looks for, in order: oidc.userinfo with userinfo_served for Ava and then for Ben, the access token lifetime change and its restore, oauth.revoke with access_token_found for lab-collage, and oidc.userinfo rejected with insufficient_scope.

The invalid_token refusals (the Lab Mail token, the expired token and the revoked token) are not in Audit, because the tenant cannot attribute a token it does not hold as active. Find them in Logs as oidc.userinfo rejected with invalid_token. The subject mismatch in step 4 appears only in your relying party's own record.

Cleanup

Confirm that the lab-collage access token manager has its original lifetime. Run unset AVA_IDT AVA_AT BEN_AT MAIL_AT TOKEN ID_TOKEN RESP and unset -f match_subject.

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