Skip to content

feat(microsoft): map userPrincipalName to preferred_username claim#4725

Open
matzegebbe wants to merge 1 commit intodexidp:masterfrom
matzegebbe:feat/microsoft-preferred-username
Open

feat(microsoft): map userPrincipalName to preferred_username claim#4725
matzegebbe wants to merge 1 commit intodexidp:masterfrom
matzegebbe:feat/microsoft-preferred-username

Conversation

@matzegebbe
Copy link
Copy Markdown
Contributor

Overview

Map userPrincipalName from the Microsoft Graph API to the preferred_username claim in Dex-issued tokens.

What this PR does / why we need it

The Microsoft connector fetches userPrincipalName via the Graph API (/v1.0/me?$select=id,displayName,userPrincipalName) but only maps it to email. The PreferredUsername field on connector.Identity is always left empty, even though other connectors (OIDC, OAuth) already populate it.

This PR sets PreferredUsername to userPrincipalName in both HandleCallback and Refresh. If the value is absent from the Graph API response, the field remains empty - no error is raised.

Why this matters: For environments that use Dex as an identity broker, preferred_username provides a reliable, human-readable identifier for consistent username mapping across downstream services (e.g. Kubernetes RBAC, GitLab, Grafana). The UPN is well-suited for this because it is always present (required, non-nullable), unique within the tenant, and already fetched by the connector.

Changes:

  • connector/microsoft/microsoft.go - set PreferredUsername to userPrincipalName in HandleCallback and Refresh
  • connector/microsoft/microsoft_test.go - update test expectation

This is a non-breaking, additive change. preferred_username will now be populated in tokens where it was previously empty. No configuration required.

Note on Azure AD / Entra ID optional claims:
This change reads userPrincipalName from the Graph API, which is always available - no optional claims configuration is needed. However, if downstream services consume the Microsoft-issued ID/access tokens directly, adding preferred_username as an optional claim in the Azure app registration (Token configuration > Add optional claim) ensures the value is present there as well.

Special notes for your reviewer

I have two questions about the current design of the Microsoft connector that go beyond this PR:

  1. Why is userPrincipalName used as the email field instead of the mail property?
    The connector has mapped userPrincipalName -> email since its initial implementation in 2017 (6193bf55). The Graph API's mail property has never been fetched. I assume this is because userPrincipalName is guaranteed to be non-null, while mail is optional and can be empty (especially for personal accounts). The emailToLowercase option (PR Added the possibility to activate lowercase for UPN-Strings #1888) was originally named upnToLowercase, which suggests this is a known trade-off.
    This also means the Go user struct field is named Email but actually holds userPrincipalName - so in this PR, identity.PreferredUsername = user.Email is setting it to the UPN, not a separate email value. There's no need for an additional Graph API field since it would just fetch the same value twice.
    However, this means the email claim in Dex-issued tokens may contain a UPN that differs from the user's actual mailbox address. Would it make sense to fetch mail from the Graph API and prefer it when available, falling back to UPN? Or is the current behavior intentional?

  2. Would a claimMapping config be a better approach?
    The OIDC and OAuth connectors support a claimMapping configuration (e.g. preferred_username: preferred_username) that lets users control which upstream claim maps to which identity field. Instead of hardcoding the UPN -> preferred_username mapping, would it be preferred to add claimMapping support to the Microsoft connector as well? This would give users flexibility to map any Graph API user property to any identity field.

If this has already been addressed and I missed it, feel free to close this.

Signed-off-by: Mathias Gebbe <mathias.gebbe@gmail.com>
@matzegebbe matzegebbe force-pushed the feat/microsoft-preferred-username branch from 4458c9a to e90840f Compare April 7, 2026 11:31
Copy link
Copy Markdown
Member

@nabokihms nabokihms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current solution doesn't seem right, because it looks like a very specific hack. On the other hand, we do not implement claim mappings for well-known providers because the fields are usually pretty standard.

I would suggest a hybrid approach, as we do for gitlab or crowd.

EmailVerified: true,
UserID: user.ID,
Username: user.Name,
PreferredUsername: user.Email,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we use the same attribute for the other claim, it looks... kinda odd.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't check Crowd. That implementation looks good to me.

Copy link
Copy Markdown
Contributor Author

@matzegebbe matzegebbe Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plus that would solve my problem and not change anything for existing installations and users (in terms of token size etc)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants