OPENID CONNECT · LAB
Step up a role holder and enforce it in the relying party
Make the tenant require a second step for management role holders, show Ben gets mfa while Ava does not, and enforce a raw-download rule in the relying party: permission first, then subject, freshness and method.
Partly readyUses your lab tenant
The lesson
Builds on: Authentication context and methods.
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.
- G20 `acr` / `acr_values` and acr-driven step-up
- G19 `claims` request parameter
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.
Create the Audit reader role
Recorded as
tenant.roles.createsucceeded.Give Ben the Audit reader role
Recorded as
tenant.users.management_roles.assignsucceeded.Ben's password sign-in needs a second step
Recorded as
oauth.authorizesucceeded (enrollment_requiredorsecond_step_required) forlab-collageabout[email protected].Ben's step-up sign-in returns a code
Recorded as
oauth.authorizesucceeded (code_issued) forlab-collageabout[email protected].See the tenant refuse a step-up by someone else
Recorded as
oauth.authorizerejected (id_token_hint_mismatch) forlab-collage.
Setup
The provider-driven half of step-up is real: your tenant can require a second step from anyone holding a management role. A relying party cannot yet ask for a class with acr_values or an essential acr claim (G20, G19), so amr stands in for the class in your own check, clearly marked as a stand-in.
Press Start. In Lab Photos, open Roles and create a custom role Audit reader with only the permission to read Audit (
tenant.audit.read). In Users, assign it to Ben.In Authentication, keep the authenticator app Optional, turn on require a second step for management role holders, and keep "remember this browser" at 0 days.
Create the relying party's project permissions. Ben is assigned to the autumn catalogue project; Ava is not.
source ~/btl-oidc.sh
SCOPE="openid"
echo '{"autumn-catalogue": []}' > projects.json
Walkthrough
Permission first. Your download handler checks
projects.jsonfor the session's subject before anything else. Ava is not listed, so her request is refused with no sign-in at all.
can_download() { jq -e --arg sub "$1" '."autumn-catalogue" | index($sub)' projects.json > /dev/null && echo "permitted" || echo "refused: not on the project"; }
can_download "$(part "$ID_TOKEN_AVA" | jq -r .sub)"
Why it matters: a stronger sign-in can never grant access that permission denies.
The provider decides who needs more. Sign Ben in with
signin prompt=login. After his password, the tenant makes him set up an authenticator app and enter a code. Redeem and keep his token, then add him to the project:
redeem '<code>'
BEN_ID_TOKEN=$ID_TOKEN; BEN_SUB=$(part "$ID_TOKEN" | jq -r .sub)
part "$ID_TOKEN" | jq .amr
jq --arg s "$BEN_SUB" '."autumn-catalogue" += [$s]' projects.json > p.tmp && mv p.tmp projects.json
amr is ["pwd","otp","mfa"]. Ava signing in the same way, with no role and no enrolled method, gets ["pwd"].
Why it matters: here the provider's own policy decided who needs more. The relying party still sets its own rule for the download.
Send Ben back with both parts of the rule. The tenant honors
max_ageand the hint, and ignoresacr_valueswithout an error.
signin max_age=900 "id_token_hint=$BEN_ID_TOKEN" acr_values=urn%3Alab%3Aacr%3Aphishing-resistant
Why it matters: each parameter carries one part of the requirement, and only the parts the provider supports have any effect.
Check the result before releasing anything. Redeem, validate against this
NONCE, then compare with the session:
btl-lab verify "$ID_TOKEN" --issuer "$ISSUER" --audience "$CLIENT_ID" --type id --nonce "$NONCE" > /dev/null && \
part "$ID_TOKEN" | jq -r --arg sub "$BEN_SUB" '
if .sub != $sub then "FAIL other subject: discard the download, end the session"
elif (.auth_time == null or (now - .auth_time) > 930) then "FAIL sign-in older than 900 seconds"
elif ((.amr // []) | index("mfa") | not) then "FAIL method (stand-in for acr until G20)"
else "OK record new auth_time and amr, issue a new session ID, resume" end'
Why it matters: the party that set the requirement checks what came back, because the request crossed the browser. A session that now carries more authority also gets a new session identifier.
Resume the download, checking permission again. Remove Ben from
projects.jsonbetween the step-up and the resume, then runcan_download "$BEN_SUB": refused.
Why it matters: a successful step-up does not bring back access removed in the meantime.
Break it
Weaken the check: change
index("mfa")to accept["pwd"]alone, add Ava toprojects.json, and run her password-only token through it. The rule is undone.
Restore: put the mfa check back and remove Ava from projects.json.
Another person steps up. Run step 3 without the hint and sign in as Ava: your subject check prints
FAIL other subject, and the session must end. With the hint, the tenant refuses on its own: after Ava enters her password, the listener printserror=login_required.
Check your work
Press Check my progress. The checks look for the role, its assignment to Ben, Ben's password sign-in needing a second step, the code his step-up returned, and the tenant's refusal when someone else signed in under his hint.
In Audit, Ben's sign-in also shows account.enroll method_enrolled for the app he was made to set up.
Cleanup
Remove the Audit reader role from Ben, which ends his sessions and revokes his tokens, then delete the role.
Restore the second step and remember-browser settings you had.
Delete
projects.jsonand setSCOPE="openid profile email".
Missing infrastructure
G20 (
acrandacr_values) and G19 (claimsrequest parameter). With them the full lab would define aphishing-resistantclass that only passkeys with user verification satisfy, sendclaims={"id_token":{"acr":{"essential":true,"values":["urn:lab:acr:phishing-resistant"]}}}withmax_age=900, watch the tenant ask Ben for his passkey even though his session is MFA, and readacrin the new token. Removing Ben's passkey would then returnerror=unmet_authentication_requirements: your relying party keeps the existing session, leaves the download blocked, explains what is needed, and does not retry with the weakeracr_valuesform.