Better Auth feature request, written against the
feature_request.ymltemplate. Suited for GitHub: yes.
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:
- 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. - Exact-client subjects are impossible. We need
sub = f(userId, clientId), notf(userId, sectorHost). - The subject format is fixed. A deployment with an existing subject scheme cannot align
subwith the identifiers its product already uses. - There is no escape hatch.
customIdTokenClaims/customUserInfoClaimsstripsubas reserved (correctly), the userinfo endpoint only rewritessubwhenpairwiseSecretis set and the client issubjectType: "pairwise", and nothing inOAuthOptionstouches 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.
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.
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—subis 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.
We run this exact hook in production behind the pnpm patch. Happy to send a PR if the shape is acceptable.