[GH-ISSUE #7864] customIdTokenClaims values are overwritten by standard profile claims in id_token #28256

Closed
opened 2026-04-17 19:42:16 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @gustavovalverde on GitHub (Feb 8, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7864

To Reproduce

  1. Configure oauthProvider with a customIdTokenClaims hook that returns claims overlapping with standard OIDC profile claims:
oauthProvider({
  loginPage: "/login",
  consentPage: "/consent",
  customIdTokenClaims: ({ user, scopes }) => ({
    given_name: "Verified First Name",
    family_name: "Verified Last Name",
  }),
})
  1. Register an OAuth client and initiate an authorization code flow requesting openid profile scopes
  2. Exchange the authorization code for tokens at /oauth2/token
  3. Decode the id_token JWT and inspect given_name and family_name

Current vs. Expected behavior

Current: The id_token contains given_name and family_name values derived from user.name (via userNormalClaims), not the values returned by customIdTokenClaims. When user.name has no space (e.g., a wallet address like 0xB566...2B71), both fields are undefined, effectively destroying the custom claims.

This happens because createIdToken in token.ts builds the payload as:

const payload = {
  ...customClaims,  // custom claims spread first
  ...userClaims,    // standard claims overwrite custom claims!
  auth_time, acr, iss, sub, aud, nonce, iat, exp,
};

userClaims (from userNormalClaims) is spread after customClaims, overwriting any overlapping keys.

Expected: Values returned by customIdTokenClaims should take precedence over auto-derived standard claims. The correct spread order is { ...userClaims, ...customClaims }, which is how userInfoEndpoint already works:

// userinfo.ts (correct order)
return {
  ...baseUserClaims,
  ...additionalInfoUserClaims,  // custom claims win
};

What version of Better Auth are you using?

1.5.0-beta.13 (canary)

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.2.0",
    "cpuModel": "Apple M4 Pro",
    "totalMemory": "48.00 GB"
  },
  "node": { "version": "v24.12.0" },
  "packageManager": { "name": "pnpm", "version": "10.28.2" },
  "frameworks": [
    { "name": "next", "version": "16.1.6" },
    { "name": "react", "version": "19.2.4" }
  ],
  "databases": [
    { "name": "@libsql/client", "version": "0.17.0" },
    { "name": "drizzle", "version": "0.45.1" }
  ],
  "betterAuth": { "version": "1.5.0-beta.13" }
}

Which area(s) are affected?

Backend, Package

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { oauthProvider } from "@better-auth/oauth-provider";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    jwt(),
    oauthProvider({
      loginPage: "/login",
      consentPage: "/consent",
      customIdTokenClaims: ({ user, scopes }) => {
        // These values get overwritten by userNormalClaims when "profile" scope is present
        return {
          given_name: "Custom Given Name",
          family_name: "Custom Family Name",
          custom_field: "custom_value",
        };
      },
    }),
  ],
});

Additional context

This is particularly impactful for applications using wallet-based authentication where user.name is a hex address with no spaces. In that case, userNormalClaims returns given_name: undefined and family_name: undefined (since name.split(" ") yields a single element), which overwrites legitimate identity claims provided by the custom hook.

Originally created by @gustavovalverde on GitHub (Feb 8, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7864 ### To Reproduce 1. Configure `oauthProvider` with a `customIdTokenClaims` hook that returns claims overlapping with standard OIDC profile claims: ```typescript oauthProvider({ loginPage: "/login", consentPage: "/consent", customIdTokenClaims: ({ user, scopes }) => ({ given_name: "Verified First Name", family_name: "Verified Last Name", }), }) ``` 2. Register an OAuth client and initiate an authorization code flow requesting `openid profile` scopes 3. Exchange the authorization code for tokens at `/oauth2/token` 4. Decode the `id_token` JWT and inspect `given_name` and `family_name` ### Current vs. Expected behavior **Current:** The `id_token` contains `given_name` and `family_name` values derived from `user.name` (via `userNormalClaims`), **not** the values returned by `customIdTokenClaims`. When `user.name` has no space (e.g., a wallet address like `0xB566...2B71`), both fields are `undefined`, effectively destroying the custom claims. This happens because `createIdToken` in `token.ts` builds the payload as: ```typescript const payload = { ...customClaims, // custom claims spread first ...userClaims, // standard claims overwrite custom claims! auth_time, acr, iss, sub, aud, nonce, iat, exp, }; ``` `userClaims` (from `userNormalClaims`) is spread **after** `customClaims`, overwriting any overlapping keys. **Expected:** Values returned by `customIdTokenClaims` should take precedence over auto-derived standard claims. The correct spread order is `{ ...userClaims, ...customClaims }`, which is how `userInfoEndpoint` already works: ```typescript // userinfo.ts (correct order) return { ...baseUserClaims, ...additionalInfoUserClaims, // custom claims win }; ``` ### What version of Better Auth are you using? 1.5.0-beta.13 (canary) ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.2.0", "cpuModel": "Apple M4 Pro", "totalMemory": "48.00 GB" }, "node": { "version": "v24.12.0" }, "packageManager": { "name": "pnpm", "version": "10.28.2" }, "frameworks": [ { "name": "next", "version": "16.1.6" }, { "name": "react", "version": "19.2.4" } ], "databases": [ { "name": "@libsql/client", "version": "0.17.0" }, { "name": "drizzle", "version": "0.45.1" } ], "betterAuth": { "version": "1.5.0-beta.13" } } ``` ### Which area(s) are affected? Backend, Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { oauthProvider } from "@better-auth/oauth-provider"; import { jwt } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ jwt(), oauthProvider({ loginPage: "/login", consentPage: "/consent", customIdTokenClaims: ({ user, scopes }) => { // These values get overwritten by userNormalClaims when "profile" scope is present return { given_name: "Custom Given Name", family_name: "Custom Family Name", custom_field: "custom_value", }; }, }), ], }); ``` ### Additional context This is particularly impactful for applications using wallet-based authentication where `user.name` is a hex address with no spaces. In that case, `userNormalClaims` returns `given_name: undefined` and `family_name: undefined` (since `name.split(" ")` yields a single element), which overwrites legitimate identity claims provided by the custom hook.
GiteaMirror added the lockedenhancementbug labels 2026-04-17 19:42:16 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Feb 8, 2026):

It seems you are experiencing an issue with the customIdTokenClaims values being overwritten by standard profile claims in the id_token. The behavior you're encountering is indeed a known issue; the standard user claims are currently overriding the custom claims due to the order in which they are spread in the createIdToken function. This means that any claims you specify in customIdTokenClaims may not be retained if there's an overlap with standard claims from userNormalClaims.

To address this, you might want to consider raising this as a feature request or a bug to the Better Auth team if it's not already tracked in an issue. It would be beneficial for the custom claims to override the standard claims in the future, as you've suggested, since applications using wallet-based authentication often face similar challenges.

If you have not done so yet, tagging the Better Auth team members @bekacru and @ping__ in your GitHub discussion could also help you get more targeted assistance with this matter.

Do you have any further questions about how to implement custom claims or other aspects of Better Auth? I'm here to help!

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3868315614 --> @better-auth-agent[bot] commented on GitHub (Feb 8, 2026): It seems you are experiencing an issue with the `customIdTokenClaims` values being overwritten by standard profile claims in the `id_token`. The behavior you're encountering is indeed a known issue; the standard user claims are currently overriding the custom claims due to the order in which they are spread in the `createIdToken` function. This means that any claims you specify in `customIdTokenClaims` may not be retained if there's an overlap with standard claims from `userNormalClaims`. To address this, you might want to consider raising this as a feature request or a bug to the Better Auth team if it's not already tracked in an issue. It would be beneficial for the custom claims to override the standard claims in the future, as you've suggested, since applications using wallet-based authentication often face similar challenges. If you have not done so yet, tagging the Better Auth team members @bekacru and @ping__ in your GitHub discussion could also help you get more targeted assistance with this matter. Do you have any further questions about how to implement custom claims or other aspects of Better Auth? I'm here to help! _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28256