From 34dd9c9fe68aac7eda2f601bf33b73d6ebca0472 Mon Sep 17 00:00:00 2001 From: Henning Perl Date: Mon, 13 Apr 2026 14:17:30 +0200 Subject: [PATCH] docs: document upstream MFA carry-over for OIDC social sign-in Adds a new page describing how Ory carries over upstream OIDC `acr` and `amr` claims into the resulting Ory session. Operators can configure per-provider `aal2_acr_values` and `aal2_amr_values` allowlists to mark sessions as AAL2 when the upstream identity provider has already performed multi-factor authentication. The new page covers: - How Ory reads the upstream `acr` / `amr` claims and decides the session AAL. - A provider support matrix (Generic OIDC, Auth0, Okta, Keycloak, Microsoft Entra v1, Ping Identity, Google, Apple, and others). - Console and CLI configuration examples. - How to ask the upstream provider for a specific `acr` value via `acr_values` or `requested_claims`. - Sample `/sessions/whoami` payload showing the new `upstream_acr` / `upstream_amr` audit fields. - Troubleshooting tips for empty upstream claims and accidental AAL2 elevation. The Dynamic MFA / step-up authentication doc gains a short section that links to the new page so customers enforcing AAL2 discover the option. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/kratos/mfa/05_step-up-authentication.mdx | 7 + docs/kratos/social-signin/93_upstream-mfa.mdx | 207 ++++++++++++++++++ src/sidebar.ts | 1 + 3 files changed, 215 insertions(+) create mode 100644 docs/kratos/social-signin/93_upstream-mfa.mdx diff --git a/docs/kratos/mfa/05_step-up-authentication.mdx b/docs/kratos/mfa/05_step-up-authentication.mdx index 7353d74060..34adde2e55 100644 --- a/docs/kratos/mfa/05_step-up-authentication.mdx +++ b/docs/kratos/mfa/05_step-up-authentication.mdx @@ -130,3 +130,10 @@ When the user successfully provides their configured second factor: - The method, for example `totp`, is added to the Ory Session. - Ory Session Authenticator Assurance Level (AAL) is set to `aal2`. - The `authenticated_at` time is set to the time when the user provides the second factor. + +## Trust upstream MFA from a social sign-in provider + +If your project uses social sign-in and the upstream identity provider already enforces MFA, Ory can trust the upstream factor +instead of asking the user to complete a second factor again. Configure `aal2_acr_values` and `aal2_amr_values` on the social +sign-in provider to allowlist the upstream `acr` and `amr` claim values that should mark the resulting Ory session as AAL2. See +[Upstream MFA carry-over](../social-signin/93_upstream-mfa.mdx) for details. diff --git a/docs/kratos/social-signin/93_upstream-mfa.mdx b/docs/kratos/social-signin/93_upstream-mfa.mdx new file mode 100644 index 0000000000..e1f614af83 --- /dev/null +++ b/docs/kratos/social-signin/93_upstream-mfa.mdx @@ -0,0 +1,207 @@ +--- +id: upstream-mfa +title: Carry over upstream multi-factor authentication +sidebar_label: Upstream MFA carry-over +--- + +# Upstream MFA carry-over + +When a user signs in through a social sign-in provider that already enforces multi-factor authentication (MFA), Ory can trust that +second factor instead of asking for another one. This avoids redundant prompts when your project enforces AAL2 through +[step-up authentication](../mfa/05_step-up-authentication.mdx) and the upstream identity provider has already verified the user +with a strong factor. + +## How it works + +Every time a user signs in or registers through an OIDC provider, Ory reads two optional claims from the upstream ID token: + +- `acr` — the [Authentication Context Class Reference](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) reported by + the provider. It tells the relying party which authentication policy was satisfied (for example `urn:okta:loa:2fa:any` or + `http://schemas.openid.net/pape/policies/2007/06/multi-factor`). +- `amr` — the [Authentication Methods References](https://www.rfc-editor.org/rfc/rfc8176.html) array. Each entry names a factor + type the user completed (for example `pwd`, `mfa`, `otp`, `hwk`, `fpt`). + +You configure two allowlists per provider: + +- `aal2_acr_values` — if the upstream `acr` claim matches any value in this list, Ory marks the resulting session as AAL2. +- `aal2_amr_values` — if any entry in the upstream `amr` array matches a value in this list, Ory marks the resulting session as + AAL2. + +When neither list matches, the session stays at AAL1. If your project enforces `session.whoami.required_aal=highest_available`, +the user is then prompted for a Kratos-managed second factor as usual. Both fields are optional; leaving them empty preserves the +current behavior of always issuing AAL1 sessions through this provider. + +The upstream `acr` and `amr` values are persisted on the session's `authentication_methods` entry as `upstream_acr` and +`upstream_amr` for auditing. They appear on `/sessions/whoami` and in webhook payloads, so downstream systems can record which +upstream factors were used. + +## Provider support + +Not every OIDC provider returns `acr` or `amr`. The table below lists the providers Ory ships with and whether their ID tokens +include these claims. + +| Provider | `acr` | `amr` | Notes | +| --------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Generic OIDC | Yes | Yes | Works with any OIDC-compliant IdP that emits these claims (Keycloak, Microsoft Entra v1, PingFederate, Auth0 federation, Okta federation, and many others). | +| Microsoft (Entra ID / Azure AD) | v1.0 only | v1.0 only | v1.0 ID tokens include both. v2.0 tokens typically omit `acr` and `amr`. | +| Auth0 | Yes | Yes | `amr` contains `mfa` once the user completed an MFA challenge. The standard step-up `acr` value is `http://schemas.openid.net/pape/policies/2007/06/multi-factor`. | +| Okta (federation) | Yes | Yes | Predefined `acr` values such as `urn:okta:loa:1fa:any` and `urn:okta:loa:2fa:any`. `amr` values include `pwd`, `mfa`, `sms`, `hwk`, `fpt`, `face`. | +| Keycloak | Yes | Yes (recent versions) | `acr` is mapped from the Level-of-Authentication config. `amr` is populated by the dedicated AMR protocol mapper. | +| Ping Identity (PingAM, PingFederate, PingOne AIC) | Yes (opt-in) | Yes (opt-in) | Neither claim is added by default. Configure them through Authentication Journeys or "Force ACR in ID Token". | +| Google | No | No | Google's OIDC implementation does not issue `acr` or `amr`. | +| Apple (Sign in with Apple) | No | No | Not part of Apple's documented ID-token schema. | +| Facebook, GitHub, GitLab, Discord, Slack, Spotify, Amazon, LinkedIn, Yandex, VK, NetID, Dingtalk, Patreon | No | No | OAuth2-only or non-standard OIDC; no `acr` or `amr` surfaced. | + +You can still configure the allowlist for any provider — Ory simply records the upstream values and never matches when the +provider doesn't emit them. + +## Configuration + +```mdx-code-block +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +``` + +1. Go to . +2. Open the social sign-in provider you want to configure, or add a new one. +3. In the provider configuration dialog, find the **Upstream MFA** fields: + - **Elevate session to AAL2 when `acr` matches** — add the upstream `acr` claim values that should mark the resulting session + as AAL2. + - **Elevate session to AAL2 when `amr` matches** — add the upstream `amr` values (per RFC 8176) that should mark the session + AAL2 when any of them appears in the upstream `amr` array. +4. Click **Save Configuration**. + +```mdx-code-block + + +``` + +1. Get the Ory Identities configuration from your project and save it to a file: + + ```shell + ory get identity-config --project --workspace --format yaml > identity-config.yaml + ``` + +2. Add `aal2_acr_values` and/or `aal2_amr_values` to the provider configuration: + + ```yaml title="identity-config.yaml" + selfservice: + methods: + oidc: + enabled: true + config: + providers: + - id: my-okta + provider: generic + client_id: ... + client_secret: ... + issuer_url: https://example.okta.com + mapper_url: base64://... + scope: + - openid + - email + # Upstream MFA carry-over: any of these acr values + # marks the resulting Ory session as AAL2. + aal2_acr_values: + - urn:okta:loa:2fa:any + - http://schemas.openid.net/pape/policies/2007/06/multi-factor + # Or match on the upstream amr array. Any entry that + # matches one of these values elevates the session. + aal2_amr_values: + - mfa + - otp + - hwk + ``` + +3. Update the Ory Identities configuration: + + ```shell + ory update identity-config --project --workspace --file identity-config.yaml + ``` + +```mdx-code-block + + +``` + +### Request a specific ACR from the provider + +Some providers (notably Auth0 and Okta) only emit a strong `acr` claim when the relying party explicitly asks for it through the +`acr_values` parameter. You can do this in two ways: + +- Pass `acr_values` as an [upstream parameter](./05_generic.mdx) on the login flow. Ory forwards the value to the provider's + authorization endpoint. +- Use the `requested_claims` field on the provider configuration to ask for the `acr` claim through the OpenID Connect + [claims parameter](https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter): + + ```yaml + selfservice: + methods: + oidc: + config: + providers: + - id: my-okta + provider: generic + # ... + requested_claims: + id_token: + acr: + essential: true + values: + - urn:okta:loa:2fa:any + aal2_acr_values: + - urn:okta:loa:2fa:any + ``` + +## Inspect the result + +After a successful sign-in, the Ory session reflects the upstream MFA decision. For example, fetching `/sessions/whoami` returns: + +```json +{ + "id": "...", + "active": true, + "authenticator_assurance_level": "aal2", + "authentication_methods": [ + { + "method": "oidc", + "aal": "aal2", + "completed_at": "2026-04-13T08:23:19Z", + "provider": "my-okta", + "upstream_acr": "urn:okta:loa:2fa:any", + "upstream_amr": ["pwd", "mfa"] + } + ], + "identity": { + /* ... */ + } +} +``` + +When the upstream `acr` or `amr` does not match the configured allowlists, the session stays at AAL1 and the upstream values are +still recorded under `upstream_acr` / `upstream_amr` for auditing. + +## What this does not do + +- It does not call the upstream provider to perform MFA on demand. Ory only inspects what the provider already reported. +- It does not change how Kratos-managed second factors (TOTP, WebAuthn, lookup secrets) work. Local step-up still applies when the + upstream did not satisfy the configured allowlist. +- It does not retroactively re-classify existing sessions. Only sessions issued after the new configuration becomes active are + evaluated against the allowlists. + +## Troubleshooting + +**The session is still AAL1 after a successful login.** Check that the upstream provider actually emits the claim. You can inspect +the raw ID token claims from the Jsonnet data mapper through `std.extVar('claims').raw_claims.acr` and +`std.extVar('claims').raw_claims.amr`. If the provider doesn't surface the claim, request it explicitly through `acr_values` or +`requested_claims`. + +**The `upstream_acr` field is empty even though the upstream sent it.** Make sure your provider config has +`claims_source: id_token` (the default) or, if you set `claims_source: userinfo`, that the userinfo endpoint also returns the +`acr` claim. Some providers only include `acr`/`amr` in the ID token. + +**The session is AAL2 but the user never completed MFA.** Audit the configured allowlists carefully. An `acr` value such as `0` or +`urn:mace:incommon:iap:bronze` typically means "no MFA was performed" — do not include those values in `aal2_acr_values`. diff --git a/src/sidebar.ts b/src/sidebar.ts index 73aec49ab1..b266b98dd4 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -63,6 +63,7 @@ const oidcSSO: SidebarItemConfig = { ], }, "kratos/social-signin/data-mapping", + "kratos/social-signin/upstream-mfa", "kratos/social-signin/account-linking", "kratos/social-signin/get-tokens", "identities/sign-in/social-sign-in/redirect-url",