Skip to content

feat(oidc): support Microsoft Entra ID email verification claim (xms_edov) #26380

@ericpaulsen

Description

@ericpaulsen

Problem

PR #25713 closed a real security issue (a silent type-assertion bypass on the email_verified OIDC claim that effectively treated several edge cases as verified). The fix is correct: an absent claim, an unrecognized type, or a non-truthy value is now fail-closed.

However, Microsoft Entra ID (Azure AD) does not emit email_verified at all by default, so any deployment using Entra as its OIDC provider stops authenticating after the upgrade. The only escape hatch today is CODER_OIDC_IGNORE_EMAIL_VERIFIED=true, which disables the protection globally, including for IdPs that do emit the claim correctly. Microsoft is one of the most common enterprise SSO IdPs for Coder customers, and they should not have to choose between upgrading and keeping verification on.

Microsoft's intended equivalent is the optional xms_edov (Email Domain Owner Verified) claim, introduced as part of their nOAuth mitigation guidance. It is a boolean indicating whether the email's domain owner has been verified by the tenant. See:

Proposed solution

Two non‑mutually‑exclusive options, both small and IdP‑agnostic:

Option A (preferred): configurable claim field

Add CODER_OIDC_EMAIL_VERIFIED_FIELD (default: email_verified), mirroring the existing CODER_OIDC_EMAIL_FIELD / CODER_OIDC_USERNAME_FIELD / CODER_OIDC_NAME_FIELD pattern. Entra customers set it to xms_edov; everyone else does nothing.

Option B: built-in Entra fallback

If email_verified is absent from the merged claims, also check xms_edov before falling back to unverified. Documented as a built-in convenience so the common Entra path works out of the box.

Shipping A alone is enough; A + B is the most ergonomic.

Implementation pointers

Touch points are localized:

  • coderd/userauth.go around the existing check (currently mergedClaims["email_verified"] lookup). Use the configured field name and keep the existing coerceEmailVerified() helper.
  • codersdk/deployment.go to add the new OIDCConfig.EmailVerifiedField serpent.String and its CODER_OIDC_EMAIL_VERIFIED_FIELD flag, defaulting to email_verified.
  • cli/server.go to wire the value into the OIDCConfig passed to coderd.
  • Docs: docs/admin/users/oidc-auth.md should grow a short Entra section explaining the xms_edov opt‑in and the new env var.

Tests should cover: default behavior unchanged, configurable field reads from the named claim, fail‑closed when the named claim is absent, and the type‑coercion contract from #25713 still applies regardless of which field is used.

Operator workaround until this lands

For single‑tenant Entra deployments, the nOAuth attack vector isn't reachable (it requires multi‑tenant token acceptance), so CODER_OIDC_IGNORE_EMAIL_VERIFIED=true combined with a pinned tenant issuer URL and CODER_OIDC_EMAIL_DOMAIN allowlist is an acceptable temporary posture.

For multi‑tenant Entra deployments, disabling verification is not acceptable. The only safe stopgap today is an Entra claims mapping policy that emits email_verified derived from xms_edov, which is significantly more work than a config flag.

Alternatives considered

  • Mirror IBM mcp-context-forge #4391 (recognize verified_primary_email / verified_secondary_email): those are Microsoft Account (MSA / consumer) claims, not workforce Entra ID claims. Doing this would not unblock typical enterprise Coder deployments and would add IdP‑specific code for a narrow case. The xms_edov claim is the correct primary signal for workforce Entra.
  • Auto‑detect Entra and special‑case it: brittle, surprises customers using non‑standard issuer URLs (e.g. behind broker IdPs), and conflicts with Coder's existing IdP‑agnostic config model.

References


Filed on behalf of a customer by Coder Agents.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureneeds-evaluationUse this label on feature requests that require joint evaluation by Product and Engineering teams.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions