OIDC MFA verification
What this is
When a user signs in to fremforge via your organisation’s IdP (Entra, Okta, Google Workspace, Authentik, Keycloak, …), the IdP returns an OIDC ID token. That token carries an amr claim (Authentication Methods References, RFC 8176) which lists the auth factors used in this sign-in — e.g. ["pwd","mfa"], ["fido"], or ["otp"].
fremforge can:
- Request a specific authentication context from your IdP at sign-in time by sending the OIDC
acr_valuesparameter in the authorization request, and - Verify the returned ID token’s
amrclaim contains an MFA-asserting value before letting the session start.
Together this turns “we trust your IdP did MFA” into “we cryptographically verified your IdP did MFA on this hop, every hop”.
Why this replaces “Trust IdP MFA”
The legacy Trust IdP MFA checkbox was an attestation: the operator confirms their IdP enforces MFA, fremforge skips its own MFA prompt for users from that source. There was no way for fremforge to verify the attestation — if the IdP’s MFA policy was misconfigured or temporarily relaxed, users would sign in without MFA and fremforge would never know.
The new OIDC MFA verification path inspects each ID token’s amr claim and refuses sign-ins that don’t assert MFA. The IdP’s signature on the ID token is what makes this trustworthy — a sign-in that lands on fremforge with amr=["mfa"] cryptographically proves your IdP performed MFA on this exact session, not “policy says it should have”.
The Trust IdP MFA checkbox is kept for back-compat — tenants whose IdP isn’t yet configured with the right ACR values can flip OIDC MFA verification off and fall back to the attestation. The next round of housekeeping will retire it.
Enabling verification
- From your org’s SSO admin page (
<your-org>/_admin/sso), find the auth source card for the IdP. - Expand the OIDC MFA verification panel.
- Tick Require MFA on every sign-in (verified).
- Pick a Vendor preset matching your IdP. The preset sets the
acr_valueswe’ll send the IdP at sign-in. - Save.
Once saved, the patched Forgejo runtime starts checking amr on every callback from this source. The first sign-in after the change is the canary — verify it succeeds, then roll out to the rest of the org.
Vendor presets
| Vendor | Preset | acr_values sent | What the IdP needs |
|---|---|---|---|
| Microsoft Entra ID | entra_c2 | c2 | Conditional Access policy mapping an authentication context to MFA. Apple/Mac users: install Microsoft Authenticator if you don’t already have a registered factor. |
| Microsoft Entra ID | entra_c3 | c3 | Same as c2 plus smart-card / client-certificate. Use for regulated tenants (gov, banking). |
| Okta | okta_phr | phr | Authentication Policy enforcing a phishing-resistant factor (FIDO2, WebAuthn). |
| Okta | okta_phrh | phrh | Same as phr but hardware-bound — YubiKey or platform authenticator. Strongest Okta ACR. |
| Google Workspace | google_mfa | urn:google:multi_factor | 2-Step Verification enforced in the Admin console. |
| Generic OIDC | empty | (none) | Just verify amr contains an MFA value. Use when your IdP doesn’t expose ACR config but does emit MFA in amr. |
| Custom | your value | (whatever you type) | Anything not on the list — Keycloak, PingFederate, ForgeRock, Authelia, … |
What MFA values we accept
Per RFC 8176 plus the common vendor extensions, the following amr values are accepted as MFA-asserting:
mfa, otp, hwk, fido, sms, tel, mca, sc, otpasswd, swk, user, wia, pop, eye, iris, face, fpt, kba
A sign-in is accepted when the amr array contains at least one of these. A sign-in with amr=["pwd"] only is rejected (password without MFA).
Alternatively, when you’ve configured a vendor preset, a sign-in is also accepted when the ID token’s acr claim equals the preset’s value exactly — e.g. acr=c2 on Entra. This is the path most Entra/Okta setups use because vendors typically return a richer acr than amr.
Testing the verification
A safe rollout: enable verification on one auth source (one tenant), have a test user sign in once. If the sign-in lands successfully, you’ve verified the IdP’s claim chain matches what we expect. If the sign-in is refused with a “MFA was not asserted” flash, the IdP returned something we didn’t recognise — common causes:
- Entra: Conditional Access policy is not bound to the application this auth source represents. Check the Entra portal → Conditional Access → Policies → confirm the app is in scope.
- Okta: Authentication Policy for the OIDC application is set to “Any factor type allowed”. Tighten to a phishing-resistant factor.
- Google Workspace: 2-Step Verification is configured per-user, not enforced organisation-wide. Set the OU’s enforcement to “On”.
The Forgejo log line for a rejected sign-in (audit-log row oauth_signin_mfa_rejected) carries the exact amr + acr values the IdP returned — copy them into a support ticket if you need help diagnosing.
Audit trail
Each enable/disable of OIDC MFA verification on an auth source emits an audit event of action sso.auth-source.oidc-mfa-config.update with previous and next values for both acr_values and require_oidc_amr_mfa, plus the actor and timestamp.
Each sign-in that’s rejected because amr didn’t assert MFA emits tenant.auth_policy.oidc_mfa.rejected with the source name, the actor’s email, and the IdP-returned amr/acr so the operator can diagnose configuration mismatches before they accumulate.
Related
- Authentication policy — broader posture controls (SSH disabled, max PAT lifetime, signed commits, …).
- SSO setup walkthrough — initial wiring of an OIDC auth source. Run this first; OIDC MFA verification is the hardening step after the basic wiring works.
- What MFA semantics mean — fremforge’s overall MFA posture and how IdP-side MFA interacts with Forgejo-side TOTP/WebAuthn.