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

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.

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. Create the Audit reader role

    Recorded as tenant.roles.create succeeded.

  2. Give Ben the Audit reader role

    Recorded as tenant.users.management_roles.assign succeeded.

  3. Ben's password sign-in needs a second step

    Recorded as oauth.authorize succeeded (enrollment_required or second_step_required) for lab-collage about [email protected].

  4. Ben's step-up sign-in returns a code

    Recorded as oauth.authorize succeeded (code_issued) for lab-collage about [email protected].

  5. See the tenant refuse a step-up by someone else

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

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

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

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

  1. Permission first. Your download handler checks projects.json for 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.

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

  1. Send Ben back with both parts of the rule. The tenant honors max_age and the hint, and ignores acr_values without 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.

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

  1. Resume the download, checking permission again. Remove Ben from projects.json between the step-up and the resume, then run can_download "$BEN_SUB": refused.

Why it matters: a successful step-up does not bring back access removed in the meantime.

Break it

  1. Weaken the check: change index("mfa") to accept ["pwd"] alone, add Ava to projects.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.

  1. 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 prints error=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

  1. Remove the Audit reader role from Ben, which ends his sessions and revokes his tokens, then delete the role.

  2. Restore the second step and remember-browser settings you had.

  3. Delete projects.json and set SCOPE="openid profile email".

Missing infrastructure

  • G20 (acr and acr_values) and G19 (claims request parameter). With them the full lab would define a phishing-resistant class that only passkeys with user verification satisfy, send claims={"id_token":{"acr":{"essential":true,"values":["urn:lab:acr:phishing-resistant"]}}} with max_age=900, watch the tenant ask Ben for his passkey even though his session is MFA, and read acr in the new token. Removing Ben's passkey would then return error=unmet_authentication_requirements: your relying party keeps the existing session, leaves the download blocked, explains what is needed, and does not retry with the weaker acr_values form.

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