Assurance levels & step-up
Assurance levels (AAL) let a realm require a minimum authentication strength before it will
issue tokens. SemAuth models three levels and emits the achieved level as the standard OIDC acr claim.
The three levels
| Level | acr value | Meaning | Reachable by |
|---|---|---|---|
| AAL1 | aal1 | Single factor. | Magic link, or federation. |
| AAL2 | aal2 | Multi-factor. | Passkey (today). One-time-code MFA is planned. |
| AAL3 | aal3 | Phishing-resistant. | Passkey. |
How each sign-in method earns a level
The level is computed when the user finishes signing in and stored on their session — it is not re-derived later.
| Sign-in method | Level earned |
|---|---|
| Passkey | AAL3 — passkeys are phishing-resistant. |
| Magic link | AAL1. |
| Federation (Google, etc.) | AAL1 today (upstream assurance mapping is not yet wired). |
Because a passkey is currently the only way to exceed AAL1, in practice "Require MFA" and "Require phishing-resistant" both mean "passkey required" right now. As one-time-code (TOTP) MFA is added, AAL2 will also be reachable with magic link + code.
Setting the policy
A realm's floor is the Minimum assurance setting on the realm
settings page (or min_aal via the Admin API). SemAuth refuses
to set a floor of AAL2/AAL3 unless passkeys are enabled, so you can't create an impossible policy that locks
everyone out.
What the floor actually does
The floor is enforced at one precise point: token issuance at /authorize. It is
not enforced by hiding sign-in methods. This is deliberate — a brand-new user must always be able to reach
an AAL1 session so they can then enroll a stronger factor. There's no chicken-and-egg lockout.
Step-up in practice
Suppose a realm requires AAL3 (passkey) and a user signs in with a magic link. They land in an AAL1 session, which doesn't meet the floor — so SemAuth bounces them to passkey enrollment. Creating the passkey both registers the credential and upgrades their session to AAL3 in one step, which then satisfies the floor and the original sign-in proceeds. The "Skip" option on the enrollment page is hidden whenever the realm requires AAL2+.
The acr and amr claims
Your app can read the assurance level from the ID token:
acr— the achieved level:aal1,aal2, oraal3.amr— the mechanisms used (RFC 8176): passkey →["hwk","user"], magic link →["otp"], federation →["fed","{provider}"].
Use these in your application to gate sensitive operations — for example, require acr = aal3 before
allowing a money transfer, independent of the realm's baseline floor.
Coming later. Relying-party-driven step-up via the standard acr_values
request parameter (so your app can demand a higher level on a specific request) is designed but not yet
implemented. Today the floor is set per realm.