[GH-ISSUE #4739] Incremental Scopes with Microsoft #10041

Closed
opened 2026-04-13 05:55:48 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @AcidRaZor on GitHub (Sep 18, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4739

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I have the following setup in Nuxt, works well to sign in, but the scopes saved in my database includes all delegated scopes approved before by the user or their admin, not what was requested.

async function signIn() {
await authClient.signIn.social({
provider: "microsoft",
callbackURL: "/dashboard",
});
}

Current vs. Expected behavior

Scopes, when disableDefaultScope is false, are correctly passed through the Oauth sign-in URL: ["openid", "profile", "email", "User.Read", "offline_access"]

When it returns, it includes all previously approved scopes EXCEPT for offline_access. This means by default the refresh token is configured to be a session-based refresh token, when I specifically have offline_access configured for a longer-lived refresh token that can only be invalidated if the user decides to change their password or if the expiry hits (they haven't logged in for an extended period of time, usually 90 days)

I'm trying to get incremental scopes working to start with minimal amount of scopes, and planned to use linkSocial for additional scopes as the user progresses through my app and have additional requirements based on their feature usage.

This may not be a bug, but a massive misunderstanding from my end on how this works.

Why would offline_access not be saved as part of the scopes in the database?

I require that for the non-session-based refresh token to run some background jobs every so often on behalf of the user as part of my feature set.

What version of Better Auth are you using?

1.3.9

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Enterprise",
    "release": "10.0.26100",
    "cpuCount": 32,
    "cpuModel": "Intel(R) Core(TM) i9-14900HX",
    "totalMemory": "31.71 GB",
    "freeMemory": "6.00 GB"
  },
  "node": {
    "version": "v22.14.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "vue",
      "version": "^3.5.21"
    },
    {
      "name": "nuxt",
      "version": "^4.1.1"
    }
  ],
  "databases": [
    {
      "name": "@libsql/client",
      "version": "^0.15.15"
    },
    {
      "name": "drizzle",
      "version": "^0.44.5"
    }
  ],
  "betterAuth": {
    "version": "^1.3.9",
    "config": null
  }
}

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

Client

Auth config (if applicable)

export const auth = betterAuth({
  database: drizzleAdapter(authDb, {
    provider: "sqlite",
  }),
  plugins: [
    username(),
    organization(),
    admin(),
    twoFactor(),
  ],
  account: {
    encryptOAuthTokens: false,
    updateAccountOnSignIn: true,
    accountLinking: {
      enabled: true,
      trustedProviders: ["google", "microsoft", "dropbox", "email-password"],
      allowDifferentEmails: true,
      allowUnlinkingAll: true,
    },
  },
  socialProviders: {
    microsoft: {
      clientId: env.MICROSOFT_CLIENT_ID,
      clientSecret: env.MICROSOFT_CLIENT_SECRET,
      tenantId: "organizations",
      authority: "https://login.microsoftonline.com",
      prompt: "select_account",
      disableDefaultScope: false,
    },   
  },
});

Additional context

No response

Originally created by @AcidRaZor on GitHub (Sep 18, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4739 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I have the following setup in Nuxt, works well to sign in, but the scopes saved in my database includes all delegated scopes approved before by the user or their admin, not what was requested. async function signIn() { await authClient.signIn.social({ provider: "microsoft", callbackURL: "/dashboard", }); } ### Current vs. Expected behavior Scopes, when disableDefaultScope is false, are correctly passed through the Oauth sign-in URL: ["openid", "profile", "email", "User.Read", "offline_access"] When it returns, it includes all previously approved scopes EXCEPT for offline_access. This means by default the refresh token is configured to be a session-based refresh token, when I specifically have offline_access configured for a longer-lived refresh token that can only be invalidated if the user decides to change their password or if the expiry hits (they haven't logged in for an extended period of time, usually 90 days) I'm trying to get incremental scopes working to start with minimal amount of scopes, and planned to use linkSocial for additional scopes as the user progresses through my app and have additional requirements based on their feature usage. This may not be a bug, but a massive misunderstanding from my end on how this works. Why would offline_access not be saved as part of the scopes in the database? I require that for the non-session-based refresh token to run some background jobs every so often on behalf of the user as part of my feature set. ### What version of Better Auth are you using? 1.3.9 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Enterprise", "release": "10.0.26100", "cpuCount": 32, "cpuModel": "Intel(R) Core(TM) i9-14900HX", "totalMemory": "31.71 GB", "freeMemory": "6.00 GB" }, "node": { "version": "v22.14.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "vue", "version": "^3.5.21" }, { "name": "nuxt", "version": "^4.1.1" } ], "databases": [ { "name": "@libsql/client", "version": "^0.15.15" }, { "name": "drizzle", "version": "^0.44.5" } ], "betterAuth": { "version": "^1.3.9", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: drizzleAdapter(authDb, { provider: "sqlite", }), plugins: [ username(), organization(), admin(), twoFactor(), ], account: { encryptOAuthTokens: false, updateAccountOnSignIn: true, accountLinking: { enabled: true, trustedProviders: ["google", "microsoft", "dropbox", "email-password"], allowDifferentEmails: true, allowUnlinkingAll: true, }, }, socialProviders: { microsoft: { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET, tenantId: "organizations", authority: "https://login.microsoftonline.com", prompt: "select_account", disableDefaultScope: false, }, }, }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 05:55:48 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 18, 2025):

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

The reason 'offline_access' is not saved in your database is that Better Auth only stores the scopes and tokens actually returned by Microsoft in the token response, not just those you request in the authorization URL. If Microsoft does not return 'offline_access' or a refresh token—even if you requested it—Better Auth cannot store it. This is a common behavior with Microsoft OAuth: sometimes, if the user or admin has previously consented to certain scopes, or if the provider decides not to issue a new refresh token (for example, due to policy or incremental consent requirements), you may not get 'offline_access' or a long-lived refresh token back, even if you ask for it in your config or via incremental consent flows like linkSocial. The framework supports incremental scopes, but it can't force the provider to return a refresh token or specific scopes if Microsoft doesn't issue them in the response (source).

If you need to ensure 'offline_access' is granted, you may need to prompt the user for re-consent, or have them explicitly approve the additional scope when linking accounts. Sometimes, removing the app from the user's Microsoft account and re-authorizing can help reset consent and trigger the provider to issue a new refresh token with 'offline_access'. This is a limitation of the Microsoft OAuth implementation, not Better Auth's storage or configuration.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3305307754 --> @dosubot[bot] commented on GitHub (Sep 18, 2025): <!-- Greeting --> Hi @AcidRaZor! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The reason 'offline_access' is not saved in your database is that Better Auth only stores the scopes and tokens actually returned by Microsoft in the token response, not just those you request in the authorization URL. If Microsoft does not return 'offline_access' or a refresh token—even if you requested it—Better Auth cannot store it. This is a common behavior with Microsoft OAuth: sometimes, if the user or admin has previously consented to certain scopes, or if the provider decides not to issue a new refresh token (for example, due to policy or incremental consent requirements), you may not get 'offline_access' or a long-lived refresh token back, even if you ask for it in your config or via incremental consent flows like linkSocial. The framework supports incremental scopes, but it can't force the provider to return a refresh token or specific scopes if Microsoft doesn't issue them in the response ([source](https://github.com/better-auth/better-auth/blob/d1fe10962079c82920ea078a9847f65a59907b81/packages/better-auth/src/plugins/generic-oauth/index.ts)). If you need to ensure 'offline_access' is granted, you may need to prompt the user for re-consent, or have them explicitly approve the additional scope when linking accounts. Sometimes, removing the app from the user's Microsoft account and re-authorizing can help reset consent and trigger the provider to issue a new refresh token with 'offline_access'. This is a limitation of the Microsoft OAuth implementation, not Better Auth's storage or configuration. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/4739)
Author
Owner

@AcidRaZor commented on GitHub (Sep 18, 2025):

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

The reason 'offline_access' is not saved in your database is that Better Auth only stores the scopes and tokens actually returned by Microsoft in the token response, not just those you request in the authorization URL. If Microsoft does not return 'offline_access' or a refresh token—even if you requested it—Better Auth cannot store it. This is a common behavior with Microsoft OAuth: sometimes, if the user or admin has previously consented to certain scopes, or if the provider decides not to issue a new refresh token (for example, due to policy or incremental consent requirements), you may not get 'offline_access' or a long-lived refresh token back, even if you ask for it in your config or via incremental consent flows like linkSocial. The framework supports incremental scopes, but it can't force the provider to return a refresh token or specific scopes if Microsoft doesn't issue them in the response (source).

If you need to ensure 'offline_access' is granted, you may need to prompt the user for re-consent, or have them explicitly approve the additional scope when linking accounts. Sometimes, removing the app from the user's Microsoft account and re-authorizing can help reset consent and trigger the provider to issue a new refresh token with 'offline_access'. This is a limitation of the Microsoft OAuth implementation, not Better Auth's storage or configuration.

To reply, just mention @dosu.

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

Thanks. What is strange is offline_access is part of the consented scopes setup on Microsoft, so it should reply with it. I will try removing the user instead and try again. Otherwise I'll pop onto discord when time permits

<!-- gh-comment-id:3305346301 --> @AcidRaZor commented on GitHub (Sep 18, 2025): > Hi [@AcidRaZor](https://github.com/AcidRaZor)! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. > > The reason 'offline_access' is not saved in your database is that Better Auth only stores the scopes and tokens actually returned by Microsoft in the token response, not just those you request in the authorization URL. If Microsoft does not return 'offline_access' or a refresh token—even if you requested it—Better Auth cannot store it. This is a common behavior with Microsoft OAuth: sometimes, if the user or admin has previously consented to certain scopes, or if the provider decides not to issue a new refresh token (for example, due to policy or incremental consent requirements), you may not get 'offline_access' or a long-lived refresh token back, even if you ask for it in your config or via incremental consent flows like linkSocial. The framework supports incremental scopes, but it can't force the provider to return a refresh token or specific scopes if Microsoft doesn't issue them in the response ([source](https://github.com/better-auth/better-auth/blob/d1fe10962079c82920ea078a9847f65a59907b81/packages/better-auth/src/plugins/generic-oauth/index.ts)). > > If you need to ensure 'offline_access' is granted, you may need to prompt the user for re-consent, or have them explicitly approve the additional scope when linking accounts. Sometimes, removing the app from the user's Microsoft account and re-authorizing can help reset consent and trigger the provider to issue a new refresh token with 'offline_access'. This is a limitation of the Microsoft OAuth implementation, not Better Auth's storage or configuration. > > _To reply, just mention [@dosu](https://go.dosu.dev/dosubot)._ > > How did I do? [Good](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/6ac364f0-35da-4004-ab84-046af889c0b8?feedback_type=other)  [![Chat with Dosu](https://camo.githubusercontent.com/d9ffebb979729c9ef0fc4e3090dbae5b17cabbfec8afe378ada6b81913d6ad8a/68747470733a2f2f646f73752e6465762f646f73752d636861742d62616467652e737667)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github) [![Join Discord](https://camo.githubusercontent.com/d562c2df5e4567ea41d37f1d91a4da0839d8341037045b103b2da268998de17f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a6f696e2d3538363546323f6c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d)](https://go.dosu.dev/discord-bot) [![Share on X](https://camo.githubusercontent.com/4649f705e2396d6f328dbc8e800a0509c9c17b3b20253b36706c393df124b93d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f582d73686172652d626c61636b)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4739) Thanks. What is strange is offline_access is part of the consented scopes setup on Microsoft, so it should reply with it. I will try removing the user instead and try again. Otherwise I'll pop onto discord when time permits
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10041