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

This commit is contained in:
Alex Yang
2026-03-01 16:13:35 +09:00
committed by GitHub
parent 660443e8d7
commit feb83a7ba3
4 changed files with 3 additions and 132 deletions

View File

@@ -1045,8 +1045,7 @@ describe("oauth2", async () => {
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();
});
it("should handle issuer with trailing slash", () => {
@@ -1101,8 +1100,7 @@ describe("oauth2", async () => {
expect(auth0Config.scopes).toEqual(["openid", "profile", "email"]);
expect(auth0Config.clientId).toBe("auth0-client-id");
expect(auth0Config.clientSecret).toBe("auth0-client-secret");
expect(auth0Config.getUserInfo).toBeDefined();
expect(typeof auth0Config.getUserInfo).toBe("function");
expect(auth0Config.getUserInfo).toBeUndefined();
});
it("should handle domain with protocol prefix", () => {
@@ -1267,8 +1265,7 @@ describe("oauth2", async () => {
expect(keycloakConfig.scopes).toEqual(["openid", "profile", "email"]);
expect(keycloakConfig.clientId).toBe("keycloak-client-id");
expect(keycloakConfig.clientSecret).toBe("keycloak-client-secret");
expect(keycloakConfig.getUserInfo).toBeDefined();
expect(typeof keycloakConfig.getUserInfo).toBe("function");
expect(keycloakConfig.getUserInfo).toBeUndefined();
});
it("should handle issuer with trailing slash", () => {

View File

@@ -1,5 +1,3 @@
import type { OAuth2Tokens, OAuth2UserInfo } from "@better-auth/core/oauth2";
import { betterFetch } from "@better-fetch/fetch";
import type { BaseOAuthProviderOptions, GenericOAuthConfig } from "../index";
export interface Auth0Options extends BaseOAuthProviderOptions {
@@ -10,17 +8,6 @@ export interface Auth0Options extends BaseOAuthProviderOptions {
domain: string;
}
interface Auth0Profile {
sub: string;
name?: string;
email?: string;
email_verified?: boolean;
picture?: string;
nickname?: string;
given_name?: string;
family_name?: string;
}
/**
* Auth0 OAuth provider helper
*
@@ -50,33 +37,6 @@ export function auth0(options: Auth0Options): GenericOAuthConfig {
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,
@@ -88,6 +48,5 @@ export function auth0(options: Auth0Options): GenericOAuthConfig {
disableImplicitSignUp: options.disableImplicitSignUp,
disableSignUp: options.disableSignUp,
overrideUserInfo: options.overrideUserInfo,
getUserInfo,
};
}

View File

@@ -1,5 +1,3 @@
import type { OAuth2Tokens, OAuth2UserInfo } from "@better-auth/core/oauth2";
import { betterFetch } from "@better-fetch/fetch";
import type { BaseOAuthProviderOptions, GenericOAuthConfig } from "../index";
export interface KeycloakOptions extends BaseOAuthProviderOptions {
@@ -10,17 +8,6 @@ export interface KeycloakOptions extends BaseOAuthProviderOptions {
issuer: string;
}
interface KeycloakProfile {
sub: string;
name?: string;
email?: string;
email_verified?: boolean;
picture?: string;
preferred_username?: string;
given_name?: string;
family_name?: string;
}
/**
* Keycloak OAuth provider helper
*
@@ -50,36 +37,6 @@ export function keycloak(options: KeycloakOptions): GenericOAuthConfig {
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,
@@ -91,6 +48,5 @@ export function keycloak(options: KeycloakOptions): GenericOAuthConfig {
disableImplicitSignUp: options.disableImplicitSignUp,
disableSignUp: options.disableSignUp,
overrideUserInfo: options.overrideUserInfo,
getUserInfo,
};
}

View File

@@ -1,5 +1,3 @@
import type { OAuth2Tokens, OAuth2UserInfo } from "@better-auth/core/oauth2";
import { betterFetch } from "@better-fetch/fetch";
import type { BaseOAuthProviderOptions, GenericOAuthConfig } from "../index";
export interface OktaOptions extends BaseOAuthProviderOptions {
@@ -10,17 +8,6 @@ export interface OktaOptions extends BaseOAuthProviderOptions {
issuer: string;
}
interface OktaProfile {
sub: string;
name?: string;
email?: string;
email_verified?: boolean;
picture?: string;
preferred_username?: string;
given_name?: string;
family_name?: string;
}
/**
* Okta OAuth provider helper
*
@@ -50,33 +37,6 @@ export function okta(options: OktaOptions): GenericOAuthConfig {
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,
@@ -88,6 +48,5 @@ export function okta(options: OktaOptions): GenericOAuthConfig {
disableImplicitSignUp: options.disableImplicitSignUp,
disableSignUp: options.disableSignUp,
overrideUserInfo: options.overrideUserInfo,
getUserInfo,
};
}