[GH-ISSUE #9204] mapProfileToUser does not override email field for Microsoft provider — results in email_not_found #28634

Open
opened 2026-04-17 20:03:36 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @vrease on GitHub (Apr 15, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/9204

Is this suited for github?

  • Yes, this is suited for github

Reproduction

Reproduction:

  1. Configure socialProviders.microsoft with an Azure Entra ID tenant where users have
    preferred_username but no email claim in the ID token (common when the AD user has no mail
    attribute).

  2. Use mapProfileToUser to map preferred_username to email:

socialProviders: {                                                                                 
  microsoft: {
    clientId: "...",
    clientSecret: "...",
    tenantId: "...",                                                                                 
    mapProfileToUser: (profile) => ({                                                                
      email: profile.preferred_username || profile.email,                                            
      name: profile.name,                                                                            
    }),                                                                                            
  },
},
  1. Sign in via Microsoft — redirected back with ?error=email_not_found.

Current vs. Expected behavior:

The default getUserInfo in @better-auth/core/dist/social-providers/microsoft-entra-id.mjs does:

const userMap = await options.mapProfileToUser?.(user);                                              
return {                                                                                             
  user: {                                                                                            
    id: user.sub,                                                                                    
    name: user.name,                                                                                 
    email: user.email,      // undefined when Entra omits the claim                                  
    image: user.picture,                                                                             
    emailVerified,                                                                                   
    ...userMap               // { email: "user@example.com" } from mapProfileToUser                  
  },                                                                                                 
  data: user                                                                                         
};                                                                                                   

Workaround: Using getUserInfo instead of mapProfileToUser resolves the issue, since it completely replaces the default implementation.

Current vs. Expected behavior

Expected: The ...userMap spread overrides email: undefined with the mapped value, and the callback proceeds successfully.

Actual: The callback at dist/api/routes/callback.mjs:137 still sees email as falsy and returns email_not_found.

What version of Better Auth are you using?

1.6.3

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:48:41 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6041",
    "release": "25.3.0",
    "cpuCount": 14,
    "cpuModel": "Apple M4 Pro",
    "totalMemory": "48.00 GB",
    "freeMemory": "0.89 GB"
  },
  "node": {
    "version": "v24.13.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.2.1"
    },
    {
      "name": "react",
      "version": "19.2.4"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "^1.5.6",
    "config": {
      "basePath": "/api/auth",
      "trustedOrigins": [
        "http://localhost:3000"
      ],
      "socialProviders": {
        "microsoft": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]",
          "tenantId": "1784385b-7b3a-4b20-b351-a7ebab504a80",
          "scope": [
            "openid",
            "profile",
            "email",
            "User.Read",
            "offline_access",
            "api://fcebc9e0-6432-4572-ac32-827149a84231/access_as_user"
          ],
          "prompt": "select_account"
        }
      },
      "session": {
        "expiresIn": 604800,
        "updateAge": 86400,
        "cookieCache": {
          "enabled": true,
          "maxAge": 300,
          "strategy": "jwe"
        }
      },
      "account": {
        "storeAccountCookie": true
      },
      "pages": {
        "signIn": "/signin"
      },
      "plugins": [
        {
          "name": "next-cookies",
          "config": {
            "id": "next-cookies",
            "version": "1.6.3",
            "hooks": {
              "before": [
                {}
              ],
              "after": [
                {}
              ]
            }
          }
        }
      ]
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
  export const auth = betterAuth({                                                                     
    socialProviders: {                                                                                 
      microsoft: {                                                                                     
        clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!,                                             
        clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!,                                     
        tenantId: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID!,                                      
        mapProfileToUser: (profile) => ({                                                              
          email: profile.preferred_username || profile.email,                                          
          name: profile.name,                                                                          
        }),                                                                                            
      },                                                                                               
    },                                                                                                 
  });

Additional context

No response

Originally created by @vrease on GitHub (Apr 15, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/9204 ### Is this suited for github? - [x] Yes, this is suited for github ### Reproduction Reproduction: 1. Configure `socialProviders.microsoft` with an Azure Entra ID tenant where users have `preferred_username` but no `email` claim in the ID token (common when the AD user has no `mail` attribute). 2. Use `mapProfileToUser` to map `preferred_username` to `email`: ```ts socialProviders: { microsoft: { clientId: "...", clientSecret: "...", tenantId: "...", mapProfileToUser: (profile) => ({ email: profile.preferred_username || profile.email, name: profile.name, }), }, }, ``` 3. Sign in via Microsoft — redirected back with `?error=email_not_found`. Current vs. Expected behavior: The default `getUserInfo` in `@better-auth/core/dist/social-providers/microsoft-entra-id.mjs` does: ```js const userMap = await options.mapProfileToUser?.(user); return { user: { id: user.sub, name: user.name, email: user.email, // undefined when Entra omits the claim image: user.picture, emailVerified, ...userMap // { email: "user@example.com" } from mapProfileToUser }, data: user }; ``` **Workaround:** Using `getUserInfo` instead of `mapProfileToUser` resolves the issue, since it completely replaces the default implementation. ### Current vs. Expected behavior **Expected:** The `...userMap` spread overrides `email: undefined` with the mapped value, and the callback proceeds successfully. **Actual:** The callback at `dist/api/routes/callback.mjs:137` still sees `email` as falsy and returns `email_not_found`. ### What version of Better Auth are you using? 1.6.3 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:48:41 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6041", "release": "25.3.0", "cpuCount": 14, "cpuModel": "Apple M4 Pro", "totalMemory": "48.00 GB", "freeMemory": "0.89 GB" }, "node": { "version": "v24.13.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.2" }, "frameworks": [ { "name": "next", "version": "16.2.1" }, { "name": "react", "version": "19.2.4" } ], "databases": null, "betterAuth": { "version": "^1.5.6", "config": { "basePath": "/api/auth", "trustedOrigins": [ "http://localhost:3000" ], "socialProviders": { "microsoft": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "tenantId": "1784385b-7b3a-4b20-b351-a7ebab504a80", "scope": [ "openid", "profile", "email", "User.Read", "offline_access", "api://fcebc9e0-6432-4572-ac32-827149a84231/access_as_user" ], "prompt": "select_account" } }, "session": { "expiresIn": 604800, "updateAge": 86400, "cookieCache": { "enabled": true, "maxAge": 300, "strategy": "jwe" } }, "account": { "storeAccountCookie": true }, "pages": { "signIn": "/signin" }, "plugins": [ { "name": "next-cookies", "config": { "id": "next-cookies", "version": "1.6.3", "hooks": { "before": [ {} ], "after": [ {} ] } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { microsoft: { clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, tenantId: process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID!, mapProfileToUser: (profile) => ({ email: profile.preferred_username || profile.email, name: profile.name, }), }, }, }); ``` ### Additional context _No response_
GiteaMirror added the oauth label 2026-04-17 20:03:36 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Apr 15, 2026):

Hi @vrease! I'm Dosu and I’m helping the better-auth team.

The spread operator in the Microsoft Entra ID provider should correctly override email: undefined with the value from mapProfileToUser [1]. I investigated the code and the ordering looks correct — ...userMap is applied last.

However, there's a likely explanation: PR #5479 (merged November 2025) fixed a bug where async mapProfileToUser functions weren't being properly awaited in the generic OAuth plugin, causing the mapped result to be silently dropped [2]. Your package.json shows "better-auth": "^1.5.6" — you may be resolving to a version that predates this fix. Try upgrading:

npm install better-auth@latest

A similar provider-specific bug was also fixed for the VK provider in PR #5719, where email was checked before mapProfileToUser was applied [3]. It's possible the Microsoft provider had a similar issue in older builds.

If upgrading doesn't resolve it, overriding getUserInfo (as you've already found) is the most robust approach for Microsoft Entra ID since it lets you handle field inconsistencies directly [4]:

import { decodeJwt } from "jose";

microsoft: {
  clientId: "...",
  clientSecret: "...",
  tenantId: "...",
  getUserInfo: async (token) => {
    if (!token.idToken) return null;
    const profile = decodeJwt(token.idToken);
    return {
      user: {
        id: profile.sub as string,
        name: profile.name as string,
        email: (profile.email || profile.preferred_username) as string,
        image: profile.picture as string,
        emailVerified: (profile.email_verified as boolean) ?? false,
      },
      data: profile,
    };
  },
}

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other
Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4251266985 --> @dosubot[bot] commented on GitHub (Apr 15, 2026): <!-- Greeting --> Hi @vrease! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The spread operator in the Microsoft Entra ID provider *should* correctly override `email: undefined` with the value from `mapProfileToUser` [[1]](https://github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/core/src/social-providers/microsoft-entra-id.ts#L263-L286). I investigated the code and the ordering looks correct — `...userMap` is applied last. However, there's a likely explanation: **PR #5479** (merged November 2025) fixed a bug where async `mapProfileToUser` functions weren't being properly awaited in the generic OAuth plugin, causing the mapped result to be silently dropped [[2]](https://github.com/better-auth/better-auth/pull/5479). Your `package.json` shows `"better-auth": "^1.5.6"` — you may be resolving to a version that predates this fix. Try upgrading: ```bash npm install better-auth@latest ``` A similar provider-specific bug was also fixed for the VK provider in PR #5719, where email was checked *before* `mapProfileToUser` was applied [[3]](https://github.com/better-auth/better-auth/pull/5719). It's possible the Microsoft provider had a similar issue in older builds. If upgrading doesn't resolve it, overriding `getUserInfo` (as you've already found) is the most robust approach for Microsoft Entra ID since it lets you handle field inconsistencies directly [[4]](https://github.com/better-auth/better-auth/issues/7787): ```ts import { decodeJwt } from "jose"; microsoft: { clientId: "...", clientSecret: "...", tenantId: "...", getUserInfo: async (token) => { if (!token.idToken) return null; const profile = decodeJwt(token.idToken); return { user: { id: profile.sub as string, name: profile.name as string, email: (profile.email || profile.preferred_username) as string, image: profile.picture as string, emailVerified: (profile.email_verified as boolean) ?? false, }, data: profile, }; }, } ``` <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other#message_id=f44f3071-e33b-4152-9227-4e14ff0d20a6&feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=The%20spread%20operator%20in%20the%20Microsoft%20Entra%20ID%20provider%20%2Ashould%2A%20correctly%20override%20%60email%3A%20undefined%60%20with%20the%20value%20from%20%60mapProfileToUser%60%20%5B%5B1%5D%5D%28https%3A//github.com/better-auth/better-auth/blob/a0caa1f6f8b2b9028b2917c8515280426ed2dbe4/packages/core/src/social-providers/microsoft-entra-id.ts%23L263-L286%29.%20I%20investigated%20the%20code%20and%20the%20ordering%20looks%20correct%20%E2%80%94%20%60...userMap%60%20is%20applied%20last.%0A%0AHowever%2C%20there%27s%20a%20likely%20explanation%3A%20%2A%2APR%20%235479%2A%2A%20%28merged%20November%202025%29%20fixed%20a%20bug%20where%20async%20%60mapProfileToUser%60%20functions%20weren%27t%20being%20properly%20awaited%20in%20the%20generic%20OAuth%20plugin%2C%20causing%20the%20mapped%20result%20to%20be%20silently%20dropped%20%5B%5B2%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/5479%29.%20Your%20%60package.json%60%20shows%20%60%22better-auth%22%3A%20%22%5E1.5.6%22%60%20%E2%80%94%20you%20may%20be%20resolving%20to%20a%20version%20that%20predates%20this%20fix.%20Try%20upgrading%3A%0A%0A%60%60%60bash%0Anpm%20install%20better-auth%40latest%0A%60%60%60%0A%0AA%20similar%20provider-specific%20bug%20was%20also%20fixed%20for%20the%20VK%20provider%20in%20PR%20%235719%2C%20where%20email%20was%20checked%20%2Abefore%2A%20%60mapProfileToUser%60%20was%20applied%20%5B%5B3%5D%5D%28https%3A//github.com/better-auth/better-auth/pull/5719%29.%20It%27s%20possible%20the%20Microsoft%20provider%20had%20a%20similar%20issue%20in%20older%20builds.%0A%0AIf%20upgrading%20doesn%27t%20resolve%20it%2C%20overriding%20%60getUserInfo%60%20%28as%20you%27ve%20already%20found%29%20is%20the%20most%20robust%20approach%20for%20Microsoft%20Entra%20ID%20since%20it%20lets%20you%20handle%20field%20inconsistencies%20directly%20%5B%5B4%5D%5D%28https%3A//github.com/better-auth/better-auth/issues/7787%29%3A%0A%0A%60%60%60ts%0Aimport%20%7B%20decodeJwt%20%7D%20from%20%22jose%22%3B%0A%0Amicrosoft%3A%20%7B%0A%20%20clientId%3A%20%22...%22%2C%0A%20%20clientSecret%3A%20%22...%22%2C%0A%20%20tenantId%3A%20%22...%22%2C%0A%20%20getUserInfo%3A%20async%20%28token%29%20%3D%3E%20%7B%0A%20%20%20%20if%20%28%21token.idToken%29%20return%20null%3B%0A%20%20%20%20const%20profile%20%3D%20decodeJwt%28token.idToken%29%3B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20user%3A%20%7B%0A%20%20%20%20%20%20%20%20id%3A%20profile.sub%20as%20string%2C%0A%20%20%20%20%20%20%20%20name%3A%20profile.name%20as%20string%2C%0A%20%20%20%20%20%20%20%20email%3A%20%28profile.email%20%7C%7C%20profile.preferred_username%29%20as%20string%2C%0A%20%20%20%20%20%20%20%20image%3A%20profile.picture%20as%20string%2C%0A%20%20%20%20%20%20%20%20emailVerified%3A%20%28profile.email_verified%20as%20boolean%29%20%3F%3F%20false%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20data%3A%20profile%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%2C%0A%7D%0A%60%60%60)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/9204)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28634