Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
OIDC MFA verification

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:

  1. Request a specific authentication context from your IdP at sign-in time by sending the OIDC acr_values parameter in the authorization request, and
  2. Verify the returned ID token’s amr claim 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

  1. From your org’s SSO admin page (<your-org>/_admin/sso), find the auth source card for the IdP.
  2. Expand the OIDC MFA verification panel.
  3. Tick Require MFA on every sign-in (verified).
  4. Pick a Vendor preset matching your IdP. The preset sets the acr_values we’ll send the IdP at sign-in.
  5. 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

VendorPresetacr_values sentWhat the IdP needs
Microsoft Entra IDentra_c2c2Conditional 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 IDentra_c3c3Same as c2 plus smart-card / client-certificate. Use for regulated tenants (gov, banking).
Oktaokta_phrphrAuthentication Policy enforcing a phishing-resistant factor (FIDO2, WebAuthn).
Oktaokta_phrhphrhSame as phr but hardware-bound — YubiKey or platform authenticator. Strongest Okta ACR.
Google Workspacegoogle_mfaurn:google:multi_factor2-Step Verification enforced in the Admin console.
Generic OIDCempty(none)Just verify amr contains an MFA value. Use when your IdP doesn’t expose ACR config but does emit MFA in amr.
Customyour 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.