Skip to content

fix(generic-oauth): use discovery userinfo endpoint instead of hardcoded URLs#8223

Merged
himself65 merged 1 commit intocanaryfrom
fix/oauth-providers-use-discovery-userinfo
Mar 1, 2026
Merged

fix(generic-oauth): use discovery userinfo endpoint instead of hardcoded URLs#8223
himself65 merged 1 commit intocanaryfrom
fix/oauth-providers-use-discovery-userinfo

Conversation

@himself65
Copy link
Copy Markdown
Contributor

Summary

  • Remove hardcoded getUserInfo from okta(), auth0(), and keycloak() provider helpers
  • The framework's built-in fallback already resolves userinfo_endpoint from the OIDC discovery document, making the custom implementations redundant and error-prone
  • Fixes Okta custom authorization servers where the /oauth2/ segment was duplicated (e.g., https://xxx.okta.com/oauth2/default/oauth2/v1/userinfo)

Closes #8179

Test plan

  • All 55 existing generic-oauth.test.ts tests pass
  • No type errors in changed files

…ded URLs

Remove custom getUserInfo from okta(), auth0(), and keycloak() provider
helpers. The hardcoded userinfo URLs caused failures when the provider's
actual endpoint differed from the assumed path (e.g., Okta custom
authorization servers duplicating the /oauth2/ segment).

The genericOAuth framework already resolves userinfo_endpoint from the
OIDC discovery document, making the custom implementations redundant.

Closes #8179
@himself65 himself65 requested a review from Bekacru as a code owner March 1, 2026 03:01
Copilot AI review requested due to automatic review settings March 1, 2026 03:01
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
better-auth-demo Ignored Ignored Mar 1, 2026 3:01am
better-auth-landing Skipped Skipped Mar 1, 2026 3:01am

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 1, 2026

Open in StackBlitz

@better-auth/api-key

npm i https://pkg.pr.new/@better-auth/api-key@8223

better-auth

npm i https://pkg.pr.new/better-auth@8223

auth

npm i https://pkg.pr.new/auth@8223

@better-auth/core

npm i https://pkg.pr.new/@better-auth/core@8223

@better-auth/drizzle-adapter

npm i https://pkg.pr.new/@better-auth/drizzle-adapter@8223

@better-auth/electron

npm i https://pkg.pr.new/@better-auth/electron@8223

@better-auth/expo

npm i https://pkg.pr.new/@better-auth/expo@8223

@better-auth/i18n

npm i https://pkg.pr.new/@better-auth/i18n@8223

@better-auth/kysely-adapter

npm i https://pkg.pr.new/@better-auth/kysely-adapter@8223

@better-auth/memory-adapter

npm i https://pkg.pr.new/@better-auth/memory-adapter@8223

@better-auth/mongo-adapter

npm i https://pkg.pr.new/@better-auth/mongo-adapter@8223

@better-auth/oauth-provider

npm i https://pkg.pr.new/@better-auth/oauth-provider@8223

@better-auth/passkey

npm i https://pkg.pr.new/@better-auth/passkey@8223

@better-auth/prisma-adapter

npm i https://pkg.pr.new/@better-auth/prisma-adapter@8223

@better-auth/redis-storage

npm i https://pkg.pr.new/@better-auth/redis-storage@8223

@better-auth/scim

npm i https://pkg.pr.new/@better-auth/scim@8223

@better-auth/sso

npm i https://pkg.pr.new/@better-auth/sso@8223

@better-auth/stripe

npm i https://pkg.pr.new/@better-auth/stripe@8223

@better-auth/telemetry

npm i https://pkg.pr.new/@better-auth/telemetry@8223

@better-auth/test-utils

npm i https://pkg.pr.new/@better-auth/test-utils@8223

commit: e139bda

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the generic-oauth provider helpers (Okta/Auth0/Keycloak) to stop hardcoding userinfo URLs and instead rely on the plugin’s existing OIDC discovery-based resolution of userinfo_endpoint, fixing incorrect URLs for Okta custom authorization servers (e.g., duplicated /oauth2/).

Changes:

  • Removed provider-specific getUserInfo implementations from okta(), auth0(), and keycloak().
  • Removed now-unused profile typings and fetch imports from those provider helper modules.
  • Updated generic-oauth.test.ts expectations to reflect that these helpers no longer return a getUserInfo function.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
packages/better-auth/src/plugins/generic-oauth/providers/okta.ts Removes hardcoded userinfo fetch logic so userinfo is resolved via discovery.
packages/better-auth/src/plugins/generic-oauth/providers/auth0.ts Removes hardcoded userinfo fetch logic so userinfo is resolved via discovery.
packages/better-auth/src/plugins/generic-oauth/providers/keycloak.ts Removes hardcoded userinfo fetch logic so userinfo is resolved via discovery.
packages/better-auth/src/plugins/generic-oauth/generic-oauth.test.ts Updates helper unit tests to assert getUserInfo is no longer provided by these helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 36 to 51
// Ensure issuer ends without trailing slash for proper discovery URL construction
const issuer = options.issuer.replace(/\/$/, "");
const discoveryUrl = `${issuer}/.well-known/openid-configuration`;

const getUserInfo = async (
tokens: OAuth2Tokens,
): Promise<OAuth2UserInfo | null> => {
const userInfoUrl = `${issuer}/oauth2/v1/userinfo`;

const { data: profile, error } = await betterFetch<OktaProfile>(
userInfoUrl,
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
},
);

if (error || !profile) {
return null;
}

return {
id: profile.sub,
name: profile.name ?? profile.preferred_username ?? undefined,
email: profile.email ?? undefined,
image: profile.picture,
emailVerified: profile.email_verified ?? false,
};
};

return {
providerId: "okta",
discoveryUrl,
clientId: options.clientId,
clientSecret: options.clientSecret,
scopes: options.scopes ?? defaultScopes,
redirectURI: options.redirectURI,
pkce: options.pkce,
disableImplicitSignUp: options.disableImplicitSignUp,
disableSignUp: options.disableSignUp,
overrideUserInfo: options.overrideUserInfo,
getUserInfo,
};
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By removing the provider-specific getUserInfo, this helper now relies entirely on the generic fallback. The current fallback (routes.getUserInfo) returns a non-null object even when the userinfo request fails (e.g., betterFetch returns error / no data), which can yield an empty id and misleading downstream errors. Consider updating the shared fallback to return null when betterFetch.error is set or data is missing, so failures are handled consistently (and so callers can surface a clear user_info_is_missing error).

Copilot uses AI. Check for mistakes.
Comment on lines 36 to 51
// Ensure domain doesn't have protocol prefix
const domain = options.domain.replace(/^https?:\/\//, "");
const discoveryUrl = `https://${domain}/.well-known/openid-configuration`;

const getUserInfo = async (
tokens: OAuth2Tokens,
): Promise<OAuth2UserInfo | null> => {
const userInfoUrl = `https://${domain}/userinfo`;

const { data: profile, error } = await betterFetch<Auth0Profile>(
userInfoUrl,
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
},
);

if (error || !profile) {
return null;
}

return {
id: profile.sub,
name: profile.name ?? profile.nickname ?? undefined,
email: profile.email ?? undefined,
image: profile.picture,
emailVerified: profile.email_verified ?? false,
};
};

return {
providerId: "auth0",
discoveryUrl,
clientId: options.clientId,
clientSecret: options.clientSecret,
scopes: options.scopes ?? defaultScopes,
redirectURI: options.redirectURI,
pkce: options.pkce,
disableImplicitSignUp: options.disableImplicitSignUp,
disableSignUp: options.disableSignUp,
overrideUserInfo: options.overrideUserInfo,
getUserInfo,
};
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as Okta: now that auth0() no longer provides a custom getUserInfo, any userinfo fetch failures will go through the generic fallback. That fallback currently doesn't check betterFetch.error / empty data and may return an object with missing fields instead of null, which changes error handling behavior. Recommend hardening the shared fallback to return null on fetch errors/missing payload.

Copilot uses AI. Check for mistakes.
Comment on lines 36 to 51
// Ensure issuer ends without trailing slash for proper discovery URL construction
const issuer = options.issuer.replace(/\/$/, "");
const discoveryUrl = `${issuer}/.well-known/openid-configuration`;

const getUserInfo = async (
tokens: OAuth2Tokens,
): Promise<OAuth2UserInfo | null> => {
// Construct userinfo URL from issuer
const userInfoUrl = `${issuer}/protocol/openid-connect/userinfo`;

const { data: profile, error } = await betterFetch<KeycloakProfile>(
userInfoUrl,
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
},
);

if (error || !profile) {
return null;
}

return {
id: profile.sub,
name: profile.name ?? profile.preferred_username ?? undefined,
email: profile.email ?? undefined,
image: profile.picture,
// Keycloak provides email_verified per OIDC standard, but availability depends on configuration.
// We default to false when not provided or not configured.
emailVerified: profile.email_verified ?? false,
};
};

return {
providerId: "keycloak",
discoveryUrl,
clientId: options.clientId,
clientSecret: options.clientSecret,
scopes: options.scopes ?? defaultScopes,
redirectURI: options.redirectURI,
pkce: options.pkce,
disableImplicitSignUp: options.disableImplicitSignUp,
disableSignUp: options.disableSignUp,
overrideUserInfo: options.overrideUserInfo,
getUserInfo,
};
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that keycloak() relies on the generic userinfo fallback, userinfo request errors/missing payload currently won't produce null (the fallback doesn't check betterFetch.error / missing data). This can result in empty IDs or misleading downstream errors. Suggest updating the shared fallback to return null on fetch error or empty response to keep behavior consistent with the removed provider-specific implementation.

Copilot uses AI. Check for mistakes.
Comment on lines 1033 to 1049
describe("Okta Provider Helper", () => {
it("should return correct GenericOAuthConfig", () => {
const oktaConfig = okta({
clientId: "okta-client-id",
clientSecret: "okta-client-secret",
issuer: "https://dev-12345.okta.com/oauth2/default",
});

expect(oktaConfig.providerId).toBe("okta");
expect(oktaConfig.discoveryUrl).toBe(
"https://dev-12345.okta.com/oauth2/default/.well-known/openid-configuration",
);
expect(oktaConfig.scopes).toEqual(["openid", "profile", "email"]);
expect(oktaConfig.clientId).toBe("okta-client-id");
expect(oktaConfig.clientSecret).toBe("okta-client-secret");
expect(oktaConfig.getUserInfo).toBeDefined();
expect(typeof oktaConfig.getUserInfo).toBe("function");
expect(oktaConfig.getUserInfo).toBeUndefined();
});
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These helper tests now assert getUserInfo is undefined, but there isn’t a regression test that the sign-in flow actually uses userinfo_endpoint from discovery for these providers (the core fix for #8179). Consider adding a test that stubs discovery to return a non-default userinfo_endpoint and asserts the userinfo request hits that URL (and not a hardcoded path), especially for Okta issuers that include /oauth2/<authServerId>.

Copilot uses AI. Check for mistakes.
@himself65 himself65 added this pull request to the merge queue Mar 1, 2026
Merged via the queue into canary with commit feb83a7 Mar 1, 2026
23 checks passed
@better-auth better-auth locked as resolved and limited conversation to collaborators Mar 31, 2026
@bytaesu bytaesu added the locked Locked conversations after being closed for 7 days label Mar 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

locked Locked conversations after being closed for 7 days

Projects

None yet

Development

Successfully merging this pull request may close these issues.

okta(), auth0(), and keycloak() provider helpers hardcode userinfo URL instead of using discovery

3 participants