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

Levelacr valueMeaningReachable by
AAL1aal1Single factor.Magic link, or federation.
AAL2aal2Multi-factor.Passkey (today). One-time-code MFA is planned.
AAL3aal3Phishing-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 methodLevel earned
PasskeyAAL3 — passkeys are phishing-resistant.
Magic linkAAL1.
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.

flowchart TD Start["User arrives at /authorize"] --> HasSession{"Active session<br/>meets min_aal?"} HasSession -->|yes| Issue["Issue authorization code"] HasSession -->|"no / no session"| Login["Show login page"] Login --> Auth["User authenticates"] Auth --> Level{"Earned level<br/>≥ min_aal?"} Level -->|yes| Issue Level -->|"no (e.g. magic link on an AAL3 realm)"| StepUp["Prompt to enroll a passkey"] StepUp --> Upgrade["Passkey enrollment mints<br/>a fresh AAL3 session"] Upgrade --> Issue

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:

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.