Skip to content

Instantly share code, notes, and snippets.

@tunnckoCore
Created August 19, 2026 15:41
Show Gist options
  • Select an option

  • Save tunnckoCore/94d555aaa7e6e52e3817cc743aa9c888 to your computer and use it in GitHub Desktop.

Select an option

Save tunnckoCore/94d555aaa7e6e52e3817cc743aa9c888 to your computer and use it in GitHub Desktop.
feat(oauth-provider): hook to resolve the OIDC subject identifier per client — better-auth feature request

feat(oauth-provider): hook to resolve the OIDC subject identifier per client

Better Auth feature request, written against the feature_request.yml template. Suited for GitHub: yes.

Is your feature request related to a problem? Please describe.

The oauth-provider plugin gives no control over the OIDC sub value. There are exactly two built-in behaviors: public subjects (sub = user.id) or sector-based pairwise (subjectType: "pairwise" + pairwiseSecret, HMAC over the redirect-URI host and the user id).

For an identity broker that isn't enough:

  1. Sector pairwise collides for native/MCP clients. The sector identifier derives from the redirect-URI host. Loopback redirects (localhost / 127.0.0.1) are the norm for CLIs, desktop apps, and MCP clients — they all share one sector, so distinct clients receive the same pairwise subject, which defeats pairwise.
  2. Exact-client subjects are impossible. We need sub = f(userId, clientId), not f(userId, sectorHost).
  3. The subject format is fixed. A deployment with an existing subject scheme cannot align sub with the identifiers its product already uses.
  4. There is no escape hatch. customIdTokenClaims / customUserInfoClaims strip sub as reserved (correctly), the userinfo endpoint only rewrites sub when pairwiseSecret is set and the client is subjectType: "pairwise", and nothing in OAuthOptions touches subject resolution.

Because of this we have shipped a pnpm patch of @better-auth/oauth-provider since 1.7.0-rc.1, and it must be re-rolled for every release. We audited rc.5, rc.6, 1.7.0, and 1.7.1: nothing changed in this area, and the 1.7.0 dist-chunk restructuring makes each re-roll manual. The patch is the only reason we are still pinned to 1.7.0-rc.4.

Describe the solution you'd like

An optional hook in OAuthOptions, called after the built-in default is computed, on OIDC-facing surfaces only:

resolveSubjectIdentifier?: (input: {
  userId: string;
  clientId: string;
  subjectType: "public" | "pairwise" | undefined;
  use: "id_token" | "userinfo" | "logout_token";
  defaultSubject: string; // what the built-in logic produced
}) => Awaitable<string>;

Call sites: id_token creation, the userinfo response, and back-channel logout tokens. Deliberately not access tokens or introspection — internal validation and the /userinfo user lookup need the real user.id, and resource servers need a stable global subject. The userinfo endpoint would also fetch the client and rewrite baseUserClaims.sub when the hook is set (today it only does so under pairwiseSecret). When the option is absent, behavior is unchanged.

Same shape as existing hooks like customIdTokenClaims: plain config, no new machinery.

Describe alternatives you've considered

  • subjectType: "pairwise" + pairwiseSecret — sector collisions for loopback clients, fixed output format, and introspection responses get pairwise-rewritten at presentation, breaking resource servers keyed on a global subject.
  • customIdTokenClaims / customUserInfoClaims — sub is reserved and stripped, so they can't override it.
  • Patching the package — what we do today. It works, but it's a per-release treadmill, and pnpm allows one patch file per package, so unrelated local fixes accumulate into the same blob.

Additional context

We run this exact hook in production behind the pnpm patch. Happy to send a PR if the shape is acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment