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.
- 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.
UserInfo answers for Ava's access token
Recorded as
oidc.userinfosucceeded (userinfo_served) forlab-collageabout[email protected].UserInfo answers for Ben's access token
Recorded as
oidc.userinfosucceeded (userinfo_served) forlab-collageabout[email protected].Shorten lab-collage's access token lifetime
Recorded as
tenant.oauth.managers.updatesucceeded.Restore the access token lifetime
Recorded as
tenant.oauth.managers.updatesucceeded.Revoke Ava's access token
Recorded as
oauth.revokesucceeded (access_token_found) forlab-collage.UserInfo refuses a token without openid
Recorded as
oidc.userinforejected (insufficient_scope) 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
You need
lab-collagewith itslab-collageID token and access token managers, Ava and Ben in Lab Photos,rp_discoverand the redefined helpers from the provider discovery lab, anduse_providerfrom the several providers lab. Runrp_discover "$ISSUER"and keepbtl-lab callbackrunning.Note the current lifetime of the
lab-collageaccess token manager.In Lab Photos, press Start.
Walkthrough
Sign in as Ava with
signin_url 'openid%20profile%20email'andexchange, 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_ATThe 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.
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.
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.
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.
Right provider, right endpoint. Sign in through
lab-mail-collagewithstart_attempt mail 'openid'andhandle_callback, and keep its access token:MAIL_AT=$(jq -r .access_token <<<"$RESP"). Call the Lab Photos UserInfo endpoint with it:401withBearer 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. Runrp_discover "$ISSUER"again afterwards.
When to call it. Note
expires_inon Ava's access token and that no refresh token was issued (you did not ask foroffline_access). Call UserInfo once during sign-in, copy the claims you need intoaccount.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.
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
An expired access token. Set the
lab-collageaccess token manager's lifetime to 60 seconds, sign in, wait two minutes, and call UserInfo with the new token. You get401withWWW-Authenticate: Bearer error="invalid_token".
Restore: set the lab-collage access token manager's lifetime back to the value you noted in Setup.
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.
Not a sign-in token. Run
signin_url 'photos.read',exchange, and call UserInfo with that token:403withBearer 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.