[GH-ISSUE #5496] OIDC Plugin sets incorrect issuer #10269

Closed
opened 2026-04-13 06:16:45 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @rbayliss on GitHub (Oct 22, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5496

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a backend using oidc plugin and JWT plugin.
  2. Set up a provider that uses the authorization_code flow.
  3. Mount the routes at any path other than the root (ex: /api/auth, as for NextJS).
  4. Attempt an OIDC login using any client library that verifies the iss property of the token.
  5. Verify that there is an error with parsing the token due to a claim mismatch on iss.

Current vs. Expected behavior

Current:
In the token callback, iss is set to ctx.context.options.baseURL, which is always going to be the root URL. This code is hit when using the JWT plugin.

Expected:
In the token callback, iss is set to jwtPlugin.options?.jwt?.issuer ?? ctx.context.options.baseURL.

What version of Better Auth are you using?

1.3.28

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041",
    "release": "24.6.0",
    "cpuCount": 14,
    "cpuModel": "Apple M4 Pro",
    "totalMemory": "48.00 GB",
    "freeMemory": "1.93 GB"
  },
  "node": {
    "version": "v24.9.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.5.6"
    },
    {
      "name": "react",
      "version": "19.1.0"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "^1.3.28",
    "config": {
      "database": {
        "isOpen": true,
        "isTransaction": false
      },
      "logger": {
        "level": "debug"
      },
      "basePath": "/api/auth",
      "plugins": [
        {
          "name": "next-cookies",
          "config": {
            "id": "next-cookies",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        },
        {
          "name": "jwt",
          "config": {
            "id": "jwt",
            "options": {
              "jwt": {
                "issuer": "http://localhost:4000/api/auth"
              }
            },
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "jwks": {
                "fields": {
                  "publicKey": {
                    "type": "string",
                    "required": true
                  },
                  "privateKey": {
                    "type": "string",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  }
                }
              }
            }
          }
        },
        {
          "name": "oidc",
          "config": {
            "id": "oidc",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "oauthApplication": {
                "modelName": "oauthApplication",
                "fields": {
                  "name": {
                    "type": "string"
                  },
                  "icon": {
                    "type": "string",
                    "required": false
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  },
                  "clientId": {
                    "type": "string",
                    "unique": true
                  },
                  "clientSecret": {
                    "type": "string",
                    "required": false
                  },
                  "redirectURLs": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string"
                  },
                  "disabled": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              },
              "oauthAccessToken": {
                "modelName": "oauthAccessToken",
                "fields": {
                  "accessToken": {
                    "type": "string",
                    "unique": true
                  },
                  "refreshToken": {
                    "type": "string",
                    "unique": true
                  },
                  "accessTokenExpiresAt": {
                    "type": "date"
                  },
                  "refreshTokenExpiresAt": {
                    "type": "date"
                  },
                  "clientId": {
                    "type": "string",
                    "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  },
                  "consentGiven": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      ],
      "socialProviders": {
        "cognito": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]",
          "domain": "REDACTED",
          "region": "us-east-1",
          "userPoolId": "REDACTED"
        }
      },
      "advanced": {
        "defaultCookieAttributes": {
          "secure": false
        }
      }
    }
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { DatabaseSync } from "node:sqlite";
import { nextCookies } from "better-auth/next-js";
import { jwt } from "better-auth/plugins";
import {oidcProvider, Client} from "better-auth/plugins/oidc-provider";
import creds from "@/creds.json";

export const clients: Client[] = [
  {
    clientId: "foo",
    clientSecret: "bar",
    name: "Test App",
    type: "web",
    redirectURLs: ["http://localhost:3000/authorization-code/callback"],
    disabled: false,
    skipConsent: false,
    metadata: {internal: true},
  }
]

export const auth = betterAuth({
  database: new DatabaseSync("./sqlite.db"),
  logger: {
    level: "debug",
  },
  basePath: "/api/auth",
  plugins: [
    nextCookies(),
    jwt({
      jwt: {
        issuer: "http://localhost:4000/api/auth"
      }
    }),
    oidcProvider({
      loginPage: "/auth/sign-in",
      consentPage: "/auth/consent",
      useJWTPlugin: true,
      allowDynamicClientRegistration: false,
      trustedClients: clients,
      metadata: {
        issuer: 'http://localhost:4000/api/auth'
      },
    })
  ],
  socialProviders: {
    cognito: {
      clientId: creds.client_id,
      clientSecret: creds.client_secret,
      domain: creds.domain,
      region: creds.region,
      userPoolId: creds.user_pool_id,
    }
  },
  advanced: {
    defaultCookieAttributes: {
      secure: false,
    }
  },
});

Additional context

Please note that this is a slightly different, but related bug to https://github.com/better-auth/better-auth/issues/3071 - that issue relates to the issuer when using the built-in signing mechanism. Also note that the proposed temporary workaround also fixes the issue, though.

Originally created by @rbayliss on GitHub (Oct 22, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5496 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a backend using oidc plugin and JWT plugin. 2. Set up a provider that uses the authorization_code flow. 3. Mount the routes at any path other than the root (ex: `/api/auth`, as for NextJS). 4. Attempt an OIDC login using any client library that verifies the `iss` property of the token. 5. Verify that there is an error with parsing the token due to a claim mismatch on `iss`. ### Current vs. Expected behavior Current: In [the token callback](https://github.com/better-auth/better-auth/blob/a8ef8884a8a7dbaf16413cf7e394931d7c31f7ed/packages/better-auth/src/plugins/oidc-provider/index.ts#L856-L867), `iss` is set to `ctx.context.options.baseURL`, which is always going to be the root URL. This code is hit when using the JWT plugin. Expected: In [the token callback](https://github.com/better-auth/better-auth/blob/a8ef8884a8a7dbaf16413cf7e394931d7c31f7ed/packages/better-auth/src/plugins/oidc-provider/index.ts#L856-L867), `iss` is set to `jwtPlugin.options?.jwt?.issuer ?? ctx.context.options.baseURL`. ### What version of Better Auth are you using? 1.3.28 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041", "release": "24.6.0", "cpuCount": 14, "cpuModel": "Apple M4 Pro", "totalMemory": "48.00 GB", "freeMemory": "1.93 GB" }, "node": { "version": "v24.9.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.0" }, "frameworks": [ { "name": "next", "version": "15.5.6" }, { "name": "react", "version": "19.1.0" } ], "databases": null, "betterAuth": { "version": "^1.3.28", "config": { "database": { "isOpen": true, "isTransaction": false }, "logger": { "level": "debug" }, "basePath": "/api/auth", "plugins": [ { "name": "next-cookies", "config": { "id": "next-cookies", "hooks": { "after": [ {} ] } } }, { "name": "jwt", "config": { "id": "jwt", "options": { "jwt": { "issuer": "http://localhost:4000/api/auth" } }, "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "jwks": { "fields": { "publicKey": { "type": "string", "required": true }, "privateKey": { "type": "string", "required": true }, "createdAt": { "type": "date", "required": true } } } } } }, { "name": "oidc", "config": { "id": "oidc", "hooks": { "after": [ {} ] }, "endpoints": {}, "schema": { "oauthApplication": { "modelName": "oauthApplication", "fields": { "name": { "type": "string" }, "icon": { "type": "string", "required": false }, "metadata": { "type": "string", "required": false }, "clientId": { "type": "string", "unique": true }, "clientSecret": { "type": "string", "required": false }, "redirectURLs": { "type": "string" }, "type": { "type": "string" }, "disabled": { "type": "boolean", "required": false, "defaultValue": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" } }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "accessToken": { "type": "string", "unique": true }, "refreshToken": { "type": "string", "unique": true }, "accessTokenExpiresAt": { "type": "date" }, "refreshTokenExpiresAt": { "type": "date" }, "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" } }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" } }, "userId": { "type": "string", "references": { "model": "user", "field": "id", "onDelete": "cascade" } }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" }, "consentGiven": { "type": "boolean" } } } } } } ], "socialProviders": { "cognito": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "domain": "REDACTED", "region": "us-east-1", "userPoolId": "REDACTED" } }, "advanced": { "defaultCookieAttributes": { "secure": false } } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { DatabaseSync } from "node:sqlite"; import { nextCookies } from "better-auth/next-js"; import { jwt } from "better-auth/plugins"; import {oidcProvider, Client} from "better-auth/plugins/oidc-provider"; import creds from "@/creds.json"; export const clients: Client[] = [ { clientId: "foo", clientSecret: "bar", name: "Test App", type: "web", redirectURLs: ["http://localhost:3000/authorization-code/callback"], disabled: false, skipConsent: false, metadata: {internal: true}, } ] export const auth = betterAuth({ database: new DatabaseSync("./sqlite.db"), logger: { level: "debug", }, basePath: "/api/auth", plugins: [ nextCookies(), jwt({ jwt: { issuer: "http://localhost:4000/api/auth" } }), oidcProvider({ loginPage: "/auth/sign-in", consentPage: "/auth/consent", useJWTPlugin: true, allowDynamicClientRegistration: false, trustedClients: clients, metadata: { issuer: 'http://localhost:4000/api/auth' }, }) ], socialProviders: { cognito: { clientId: creds.client_id, clientSecret: creds.client_secret, domain: creds.domain, region: creds.region, userPoolId: creds.user_pool_id, } }, advanced: { defaultCookieAttributes: { secure: false, } }, }); ``` ### Additional context Please note that this is a slightly different, but related bug to https://github.com/better-auth/better-auth/issues/3071 - that issue relates to the issuer when using the built-in signing mechanism. Also note that the [proposed temporary workaround](https://github.com/better-auth/better-auth/issues/3071#issuecomment-3401664114) also fixes the issue, though.
GiteaMirror added the lockedbug labels 2026-04-13 06:16:45 -05:00
Author
Owner

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

This is a known issue in Better Auth v1.3.28: when using the OIDC plugin with the JWT plugin and mounting at a non-root path, the iss claim in issued tokens is set to the base URL (ctx.context.options.baseURL) instead of the configured issuer (jwtPlugin.options?.jwt?.issuer). This causes token validation errors for clients expecting the issuer to match your configured value see code.

A workaround is to use the getAdditionalUserInfoClaim function in your oidcProvider config to explicitly set the iss claim to your desired issuer. For example:

oidcProvider({
  // ...other options
  getAdditionalUserInfoClaim: () => ({
    iss: "http://localhost:4000/api/auth",
    iat: Math.floor(Date.now() / 1000), // optional: fixes iat as well
  }),
})

This will overwrite the default iss value in the token payload and resolve the claim mismatch reference.

Recent versions of Better Auth (after v1.3.28) have merged fixes to ensure the OIDC plugin uses the JWT plugin's configured issuer when signing tokens see PR #2755. If you upgrade, this workaround may no longer be necessary.

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:3433578252 --> @dosubot[bot] commented on GitHub (Oct 22, 2025): <!-- Answer --> This is a known issue in Better Auth v1.3.28: when using the OIDC plugin with the JWT plugin and mounting at a non-root path, the `iss` claim in issued tokens is set to the base URL (`ctx.context.options.baseURL`) instead of the configured issuer (`jwtPlugin.options?.jwt?.issuer`). This causes token validation errors for clients expecting the issuer to match your configured value [see code](https://github.com/better-auth/better-auth/blob/fa6ee9d68214c6a51e7253b53f957407b116b0c4/packages/better-auth/src/plugins/oidc-provider/index.ts). A workaround is to use the `getAdditionalUserInfoClaim` function in your `oidcProvider` config to explicitly set the `iss` claim to your desired issuer. For example: ```ts oidcProvider({ // ...other options getAdditionalUserInfoClaim: () => ({ iss: "http://localhost:4000/api/auth", iat: Math.floor(Date.now() / 1000), // optional: fixes iat as well }), }) ``` This will overwrite the default `iss` value in the token payload and resolve the claim mismatch [reference](https://github.com/better-auth/better-auth/issues/3071#issuecomment-3401664114). Recent versions of Better Auth (after v1.3.28) have merged fixes to ensure the OIDC plugin uses the JWT plugin's configured issuer when signing tokens [see PR #2755](https://github.com/better-auth/better-auth/pull/2755). If you upgrade, this workaround may no longer be necessary. <!-- 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/c54ede13-511b-461a-af03-4d7799b540c3?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/c54ede13-511b-461a-af03-4d7799b540c3?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/c54ede13-511b-461a-af03-4d7799b540c3?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/c54ede13-511b-461a-af03-4d7799b540c3?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/c54ede13-511b-461a-af03-4d7799b540c3?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/c54ede13-511b-461a-af03-4d7799b540c3?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/c54ede13-511b-461a-af03-4d7799b540c3?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/5496)
Author
Owner

@rbayliss commented on GitHub (Oct 22, 2025):

I can confirm that this is still an issue and is not resolved as of 1.3.28. The relevant code is here.

<!-- gh-comment-id:3433900005 --> @rbayliss commented on GitHub (Oct 22, 2025): I can confirm that this is still an issue and is not resolved as of 1.3.28. The [relevant code is here](https://github.com/better-auth/better-auth/blob/canary/packages/better-auth/src/plugins/oidc-provider/index.ts#L856-L866).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10269