OPENID CONNECT · LAB
Read amr from your tenant's authentication policy
Sign in four different ways and read what amr says for each, see acr_values ignored without an error, and write a relying-party rule that treats a missing acr as falling short.
Partly readyUses your lab tenant
The lesson
Builds on: Connecting a sign-in to an account.
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.
Turn on the authenticator app and passkeys
Recorded as
tenant.authentication.updatesucceeded.Enroll an authenticator app for Ava
Recorded as
account.securitysucceeded (method_enrolled) about[email protected].Reach the second step after the password
Recorded as
oauth.authorizesucceeded (second_step_required) forlab-collageabout[email protected].Complete the second step with an app code
Recorded as
account.second_stepsucceeded (second_step_completed) about[email protected].Complete it with a recovery code instead
Recorded as
account.second_stepsucceeded (recovery_code_used) about[email protected].
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
amr in your tenant's ID tokens follows its real authentication policy. Classes of authentication (acr) are not available yet (G20), so the context half of the lesson is practised on the relying-party side.
Press Start. In Lab Photos, open Authentication and note the current values. Set the authenticator app (TOTP) to Optional, passkeys to Optional with passwordless sign-in allowed and user verification required, and the second step to ask users who have enrolled one. Keep "remember this browser" at 0 days so every sign-in asks again.
Have an authenticator app and a device that can hold a passkey ready.
source ~/btl-oidc.shand runbtl-lab callbackbefore each request.
Walkthrough
Read what the provider publishes.
GET$ISSUER/.well-known/openid-configuration
Open in console
GET $ISSUER/.well-known/openid-configurationThere is no acr_values_supported, claims_parameter_supported is false, and amr is among claims_supported.
Why it matters: a relying party can ask only for classes a provider publishes, and the meaning of each class comes from the provider's own documentation.
Password only. Sign in as Ava with
signin prompt=login, redeem, and read both claims:
part "$ID_TOKEN" | jq '{amr, acr}'
{"amr":["pwd"],"acr":null}.
Why it matters: amr lists the methods used in this sign-in, and acr is simply absent.
Password and app. At
$ISSUER/account/security, set up the authenticator app for Ava and save her recovery codes somewhere safe. Runsignin prompt=loginagain: password, then a code.amris["pwd","otp","mfa"].
Why it matters: pwd and otp name the two methods, and mfa says they counted as more than one factor.
Password and a recovery code. Repeat step 3, but at the second step choose a recovery code instead of the app.
amris again["pwd","otp","mfa"].
Why it matters: two different methods produce the same list. That is one reason the lesson keeps access decisions off amr.
A passkey. At
$ISSUER/account/security, add a passkey. Runsignin prompt=loginand choose to sign in with a passkey from the sign-in page. A synced passkey gives["swk","mfa"], a device-bound security key["hwk","mfa"].
Why it matters: providers describe passkeys differently, and neither value says "phishing-resistant" by itself. That judgment belongs in a class definition.
Ask for a class anyway.
signin prompt=login acr_values=urn%3Alab%3Aacr%3Aphishing-resistant
Sign in with the password and app code. There is no error, a normal code comes back, and the ID token has no acr.
Why it matters: the specification's minimum for acr_values is "no error", so every response needs checking.
Write the portal rule from the lesson: an allowlist of
acrvalues per action, where a missingacrmeets no requirement, andamris used only for a readable sign-in history line.
allowed() { part "$ID_TOKEN" | jq -r --arg action "$1" '
{"ordinary": ["urn:lab:acr:mfa", "urn:lab:acr:phishing-resistant"], "raw_download": ["urn:lab:acr:phishing-resistant"]} as $rules
| if (.acr != null and ($rules[$action] | index(.acr))) then "allow \($action)" else "refuse \($action): acr \(.acr // "missing")" end'; }
history_line() { part "$ID_TOKEN" | jq -r '[.amr[]? | {"pwd":"password","otp":"one-time code","swk":"passkey","hwk":"security key","mfa":null}[.] // empty] | join(" and ")'; }
allowed raw_download; history_line
Run it after each kind of sign-in from steps 2 to 6. Every token is refused for raw_download, and the history line reads, for example, "password and one-time code".
Why it matters: a class is defined by the provider's published meaning, and a method list is display and audit material, not an access decision.
Break it
"No claim means MFA". Try to remove
amrfrom ID tokens: in OAuth > ID token managers, open the manager assigned tolab-collageand look for an override foramr. There is none to choose, becauseamrandauth_timeare protected claims the tenant always derives itself. Now test a careless relying-party rule against a claims set withoutamr, the shape another provider might send:
echo '{"sub":"someone","auth_time":1790929800}' | jq -r 'if (.amr == null or (.amr | index("mfa"))) then "treated as MFA" else "not MFA" end'
It prints treated as MFA for a sign-in that proved nothing. Fix the rule so a missing claim is never evidence of one factor or two.
Restore: keep only the corrected rule in your notes and code.
Check your work
Press Check my progress. The checks look for the policy change, Ava's authenticator app enrollment, a sign-in that reached the second step, and second steps completed with an app code and with a recovery code.
Passkey enrollments appear in Audit as account.security method_enrolled too, and a passkey sign-in records account.sign_in followed by oauth.authorize code_issued, with no second step.
Cleanup
Restore the authentication values you noted in Setup.
Keep Ava's authenticator app and passkey. Step up a role holder uses the app.
One recovery code was used, so generate new ones at
$ISSUER/account/security.
Missing infrastructure
G20 (
acrandacr_values). Tenant-defined authentication context classes. The full lab would defineurn:lab:acr:mfaandurn:lab:acr:phishing-resistantin Authentication, mapped to method rules, see them inacr_values_supported, setdefault_acr_valuesonlab-collage, requestacr_values, and readacrbesideamrin the ID token. Step 7's rule would then allow the passkey sign-in.G19 (
claimsrequest parameter). Requestingacras an essential claim with a values list, to compare voluntary and essential behavior.