Incorrect capatilisation of token type (bearer) in ODIC refresh token flow #2117

Closed
opened 2026-03-13 09:28:32 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @kilakewe on GitHub (Oct 13, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Post grant_type of refresh_token to /oauth/token
  2. Recieve response

Current vs. Expected behavior

Curerntly a successful refresh_token request will return

{ access_token: '...', 
  token_type: 'bearer',
  expires_in: 3600,
  refresh_token: '...',
  scope: '...' }

The expected result is:

{ access_token: '...', 
  token_type: 'Bearer',
  expires_in: 3600,
  refresh_token: '...',
  scope: '...' }

NOTE: In the standrd authorisation code flow, the token type is "Bearer", not "bearer". There is an inconsisent use of capatalisation.

What version of Better Auth are you using?

^1.3.27

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.0.0: Wed Sep 17 21:41:26 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T6041",
    "release": "25.0.0",
    "cpuCount": 14,
    "cpuModel": "Apple M4 Pro",
    "totalMemory": "64.00 GB",
    "freeMemory": "0.11 GB"
  },
  "node": {
    "version": "v22.20.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.3"
  },
  "frameworks": [
    {
      "name": "vue",
      "version": "^3.5.22"
    },
    {
      "name": "nuxt",
      "version": "^4.1.3"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "@prisma/client",
      "version": "^6.17.1"
    }
  ],
  "betterAuth": {
    "version": "^1.3.27",
    "config": null
  }
}

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

Backend

Auth config (if applicable)

import {
  admin,
  oidcProvider,
  openAPI,
  twoFactor,
  jwt,
} from "better-auth/plugins"
import { passkey } from "better-auth/plugins/passkey"
import pg from "pg"
import { betterAuth } from "better-auth"

export const authServer = betterAuth({
  baseURL: "https://localhost:3000",
  database: new pg.Pool({
    connectionString: process.env.POSTGRES_URL,
    connectionTimeoutMillis: 2500,
    idleTimeoutMillis: 10000,
  }),
  secret: process.env.BETTER_AUTH_SECRET,
  appName: "Authentication", // provide your app name. It'll be used as an issuer.
  account: {
    accountLinking: {
      enabled: true,
      allowDifferentEmails: false,
    },
  },
  user: {
    additionalFields: {
      userTermsAccepted: {
        type: "boolean",
        required: false,
        defaultValue: false,
        input: true,
      },
      lang: {
        type: "string",
        required: false,
        defaultValue: "en",
        input: true,
      },
      givenName: {
        type: "string",
        required: false,
        defaultValue: "",
        input: true,
      },
      familyName: {
        type: "string",
        required: false,
        defaultValue: "",
        input: true,
      },
      onboardingCompleted: {
        type: "boolean",
        required: false,
        defaultValue: false,
        input: true,
      },
    } as const,
  },
  emailVerification: {
    autoSignInAfterVerification: true,
    sendOnSignUp: true,
    sendVerificationEmail: async ({ user, url }) => {
        console.log("Email verification URL:", url)
    },
  },
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false,
    autoSignIn: true,
    sendResetPassword: async ({ user, url }) => {
      console.log("Email verification URL:", url)
    },
  },
  plugins: [
    admin(),
    openAPI({
      disableDefaultReference: false,
    }),
    twoFactor({
      issuer: "Authentication",
    }),
    passkey({
      rpName: "Authentication",
    }),
    jwt({
      disableSettingJwtHeader: true,
      jwt: {
        expirationTime: "15m",
        definePayload: ({ user }) => {
          return {
            id: user.id,
            email: user.email,
            role: user.role,
            name: ((user.givenName as string || "") + " " + (user.familyName as string || "")).trim(),
            givenName: user.givenName,
            familyName: user.familyName,
            lang: user.lang,
          }
        },
      },
    }),
    oidcProvider({
      useJWTPlugin: true,
      requirePKCE: true, // require PKCE for authorization code flow
      defaultScope: "openid profile email offline_access", // default scopes
      loginPage: "/signin", // path to the login page
      allowDynamicClientRegistration: false, // allow dynamic client registration
      consentPage: "/consent",
      accessTokenExpiresIn: 60 * 60, // 1 hour
      refreshTokenExpiresIn: 60 * 60 * 24 * 7, // 7 days
      getAdditionalUserInfoClaim: (client) => {
        return { aud: client.clientId }
      },
    }),
  ],
  advanced: {
    cookiePrefix: "AUTH",
    useSecureCookies: true,
  },
  databaseHooks: {
    user: {
      create: {
        after: async (user) => {
          // Create SNS client...
          // Publish to the Object Creation Alerts SNS Topic...
          // Publish the SNS message...
        },
      },
    },
  },
  telemetry: {
    enabled: false,
  },
  disabledPaths: [
    "/token",
  ],
})

Additional context

No response

Originally created by @kilakewe on GitHub (Oct 13, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Post `grant_type` of `refresh_token` to `/oauth/token` 2. Recieve response ### Current vs. Expected behavior Curerntly a successful refresh_token request will return ``` javascript { access_token: '...', token_type: 'bearer', expires_in: 3600, refresh_token: '...', scope: '...' } ``` The expected result is: ``` javascript { access_token: '...', token_type: 'Bearer', expires_in: 3600, refresh_token: '...', scope: '...' } ``` NOTE: In the standrd authorisation code flow, the token type is "Bearer", not "bearer". There is an inconsisent use of capatalisation. ### What version of Better Auth are you using? ^1.3.27 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Wed Sep 17 21:41:26 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T6041", "release": "25.0.0", "cpuCount": 14, "cpuModel": "Apple M4 Pro", "totalMemory": "64.00 GB", "freeMemory": "0.11 GB" }, "node": { "version": "v22.20.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.3" }, "frameworks": [ { "name": "vue", "version": "^3.5.22" }, { "name": "nuxt", "version": "^4.1.3" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "@prisma/client", "version": "^6.17.1" } ], "betterAuth": { "version": "^1.3.27", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { admin, oidcProvider, openAPI, twoFactor, jwt, } from "better-auth/plugins" import { passkey } from "better-auth/plugins/passkey" import pg from "pg" import { betterAuth } from "better-auth" export const authServer = betterAuth({ baseURL: "https://localhost:3000", database: new pg.Pool({ connectionString: process.env.POSTGRES_URL, connectionTimeoutMillis: 2500, idleTimeoutMillis: 10000, }), secret: process.env.BETTER_AUTH_SECRET, appName: "Authentication", // provide your app name. It'll be used as an issuer. account: { accountLinking: { enabled: true, allowDifferentEmails: false, }, }, user: { additionalFields: { userTermsAccepted: { type: "boolean", required: false, defaultValue: false, input: true, }, lang: { type: "string", required: false, defaultValue: "en", input: true, }, givenName: { type: "string", required: false, defaultValue: "", input: true, }, familyName: { type: "string", required: false, defaultValue: "", input: true, }, onboardingCompleted: { type: "boolean", required: false, defaultValue: false, input: true, }, } as const, }, emailVerification: { autoSignInAfterVerification: true, sendOnSignUp: true, sendVerificationEmail: async ({ user, url }) => { console.log("Email verification URL:", url) }, }, emailAndPassword: { enabled: true, requireEmailVerification: false, autoSignIn: true, sendResetPassword: async ({ user, url }) => { console.log("Email verification URL:", url) }, }, plugins: [ admin(), openAPI({ disableDefaultReference: false, }), twoFactor({ issuer: "Authentication", }), passkey({ rpName: "Authentication", }), jwt({ disableSettingJwtHeader: true, jwt: { expirationTime: "15m", definePayload: ({ user }) => { return { id: user.id, email: user.email, role: user.role, name: ((user.givenName as string || "") + " " + (user.familyName as string || "")).trim(), givenName: user.givenName, familyName: user.familyName, lang: user.lang, } }, }, }), oidcProvider({ useJWTPlugin: true, requirePKCE: true, // require PKCE for authorization code flow defaultScope: "openid profile email offline_access", // default scopes loginPage: "/signin", // path to the login page allowDynamicClientRegistration: false, // allow dynamic client registration consentPage: "/consent", accessTokenExpiresIn: 60 * 60, // 1 hour refreshTokenExpiresIn: 60 * 60 * 24 * 7, // 7 days getAdditionalUserInfoClaim: (client) => { return { aud: client.clientId } }, }), ], advanced: { cookiePrefix: "AUTH", useSecureCookies: true, }, databaseHooks: { user: { create: { after: async (user) => { // Create SNS client... // Publish to the Object Creation Alerts SNS Topic... // Publish the SNS message... }, }, }, }, telemetry: { enabled: false, }, disabledPaths: [ "/token", ], }) ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 09:28:32 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 13, 2025):

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

This is a confirmed inconsistency in Better Auth: the refresh token flow sets token_type to "bearer" (lowercase), while the authorization code flow uses "Bearer" (capitalized). According to OAuth 2.0 and OIDC standards, "Bearer" (capitalized) is the correct value and should be used consistently. The relevant code in the OIDC provider plugin sets these values directly in the response JSON, which causes the mismatch. There is currently no configuration option to override this, so a code change is needed to standardize the capitalization in both flows.
You can see the relevant code for the refresh token flow setting "bearer" here.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Oct 13, 2025): <!-- Greeting --> Hi @kilakewe! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a confirmed inconsistency in Better Auth: the refresh token flow sets `token_type` to `"bearer"` (lowercase), while the authorization code flow uses `"Bearer"` (capitalized). According to OAuth 2.0 and OIDC standards, `"Bearer"` (capitalized) is the correct value and should be used consistently. The relevant code in the OIDC provider plugin sets these values directly in the response JSON, which causes the mismatch. There is currently no configuration option to override this, so a code change is needed to standardize the capitalization in both flows. You can see the relevant code for the refresh token flow setting `"bearer"` [here](https://github.com/better-auth/better-auth/blob/c3acb35269bb2881d30580a6bfc5a0dd4ccb8691/packages/better-auth/src/plugins/oidc-provider/index.ts). <!-- 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/f52a0733-f60c-44ce-a907-0bbf470fa67d?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/f52a0733-f60c-44ce-a907-0bbf470fa67d?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/f52a0733-f60c-44ce-a907-0bbf470fa67d?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/f52a0733-f60c-44ce-a907-0bbf470fa67d?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/f52a0733-f60c-44ce-a907-0bbf470fa67d?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/f52a0733-f60c-44ce-a907-0bbf470fa67d?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/f52a0733-f60c-44ce-a907-0bbf470fa67d?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/5284)
Author
Owner

@yutaka5 commented on GitHub (Oct 14, 2025):

Hi, I’m new to the project and wanted to give this a try.
I’ve opened PR #5308 to fix the incorrect token type capitalization (bearer -> Bearer) in the OIDC refresh token flow.

@yutaka5 commented on GitHub (Oct 14, 2025): Hi, I’m new to the project and wanted to give this a try. I’ve opened [PR #5308](https://github.com/better-auth/better-auth/pull/5308) to fix the incorrect token type capitalization (`bearer` -> `Bearer`) in the OIDC refresh token flow.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2117