OPENID CONNECT · LAB
Audit your tenant as an OpenID Provider
Collect evidence, duty by duty, that your tenant keeps the promises relying parties count on, and find where its metadata honestly says not implemented.
Partly readyUses both lab tenants
The lesson
Builds on: Implementing a relying party.
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.
A max_age=0 request causes a real sign-in
Recorded as
oauth.authorizesucceeded (user_signed_in) forlab-collageabout[email protected].A silent answer that keeps the old auth_time
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].Allow a hybrid response type in Flow policy
Recorded as
tenant.oauth.policy.updatesucceeded.See tokens refused in the query string
Recorded as
oauth.authorizerejected (unsupported_response_mode) forlab-collage.See a Lab Mail ID token refused as a hint
Recorded as
oauth.authorizerejected (invalid_id_token_hint) 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
Every duty in the lesson's table can be checked against your tenant with real requests and settings.
Start a checklist with the lesson's rows: issuer, keys, audience, nonce,
auth_timeandacr, subject stability, UserInfosub, consent, redirect status, metadata, and monitoring.source ~/btl-oidc.sh, runbtl-lab callbackbefore each request, and press Start.
Walkthrough
The issuer. Compare the discovery
issuer, an ID token'siss, and the callback'sissparameter character by character, including the absence of a trailing slash:
curl -s "$ISSUER/.well-known/openid-configuration" | jq -r .issuer
part "$ID_TOKEN" | jq -r .iss
Why it matters: relying parties store the issuer with every account link, so one changed character makes every customer look new.
Keys. List what the tenant publishes:
GET$ISSUER/oauth/jwks
Open in console
GET $ISSUER/oauth/jwksEvery token you have received names a kid in this set. If you did Rotate a signing key against a cached key set, the retired key appeared here until you disabled it.
Why it matters: a key is published before it signs and stays published while anything it signed is valid.
Audience and nonce. Sign in with a nonce that uses every allowed character class, then compare:
URL=$(signin); NONCE='Lab.nonce_123~x'; URL=$(sed "s/nonce=[^&]*/nonce=Lab.nonce_123~x/" <<<"$URL"); echo "$URL"
redeem '<code>'
part "$ID_TOKEN" | jq '{nonce, aud, azp}'
nonce comes back byte for byte, and aud and azp equal your client ID.
Why it matters: these copied values are exactly what the relying party's checks compare.
Honest authentication facts. Ask for
max_age=0: the password form appears andauth_timeis now. Then ask withprompt=none: a code with no page, andauth_timeunchanged. Then ask for a class with a password-only sign-in:
signin prompt=login acr_values=urn%3Alab%3Aacr%3Aphishing-resistant
amr is ["pwd"] and there is no acr.
Why it matters: a provider that wrote "now" into auth_time or overstated amr would defeat every relying party's check.
Subjects. Compare Ava's
subnow withID_TOKEN_AVAfrom the first lab, which predates her email change there, and with UserInfo:
part "$ID_TOKEN" | jq -r .sub; part "$ID_TOKEN_AVA" | jq -r .sub
curl -s -H "Authorization: Bearer $TOKEN" "$ISSUER/oidc/userinfo" | jq -r .sub
curl -s "$ISSUER/.well-known/openid-configuration" | jq .subject_types_supported
All three match, and subject_types_supported is ["public"]. In the first lab, the recreated lab-tmp-reuse user did not inherit the deleted user's sub.
Why it matters: the issuer and subject pair is what every relying-party account is built on.
The redirect status. In the developer tools' network view, submit the tenant's password form during a
signin prompt=login. The form's response is303with your callback inLocation.
Why it matters: a 307 would make the browser repeat the password POST to the relying party.
Metadata follows the real policy. In OAuth > Flow policy, allow the
implicitgrant and thecode id_tokenresponse type, and keep theform_postmode allowed. Onlab-collage, allow the same grant, response type and mode. Read discovery:
curl -s "$ISSUER/.well-known/openid-configuration" | jq '{response_types_supported, end_session_endpoint, not_implemented: [.btl_endpoint_status | to_entries[] | select(.value == "not_implemented") | .key]}'
code id_token is listed now, there is no end_session_endpoint, and the endpoints the tenant does not offer are named not_implemented.
Why it matters: discovery lists only what the tenant does today, so relying parties never build on a promise it cannot keep.
Restore: Break it uses these settings once more; Cleanup removes the implicit grant and code id_token again.
Monitoring. In Logs, open today's protocol summary for
oauth.tokenand find thecode_replayedrow from the previous lab, with its count and its first and last request IDs.
Why it matters: rates of replays and failures per client are the provider's early warning, and the summary records them without any token.
Break it
Ask the tenant to break a promise, and record each refusal:
In Signing keys, try to retire the key the ID token manager uses: refused, because it is in use.
Tokens in the query string, using step 7's settings:
RT="code id_token" signin response_mode=query
The listener prints error=invalid_request, with a description saying responses that carry tokens cannot use the query mode.
A hint the tenant did not sign for
lab-collage: sign in tolab-mail-collageat Lab Mail, then send that ID token asid_token_hintto Lab Photos, as in Suggest an account with login_hint. The listener printserror=invalid_request.
Check your work
Press Check my progress. The checks look for a real sign-in under max_age=0, a silent answer, the Flow policy change, the refused query-mode tokens and the refused foreign hint. Your checklist has one piece of evidence for each row.
Cleanup
Restore Flow policy and lab-collage to the code response type with the modes you had, and remove the implicit grant unless a later lab needs it.
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.