-
Notifications
You must be signed in to change notification settings - Fork 1
feat(auth): add auth config discovery endpoint for dynamic SSO UI rendering #1141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -335,4 +335,65 @@ export class AuthManager { | |
| get api() { | ||
| return this.getOrCreateAuth().api; | ||
| } | ||
|
|
||
| /** | ||
| * Get public authentication configuration | ||
| * Returns safe, non-sensitive configuration that can be exposed to the frontend | ||
| * | ||
| * This allows the frontend to discover: | ||
| * - Which social/OAuth providers are available | ||
| * - Whether email/password login is enabled | ||
| * - Which advanced features are enabled (2FA, magic links, etc.) | ||
| */ | ||
| getPublicConfig() { | ||
| // Extract social providers info (without sensitive data) | ||
| const socialProviders = []; | ||
| if (this.config.socialProviders) { | ||
| for (const [id, providerConfig] of Object.entries(this.config.socialProviders)) { | ||
| if (providerConfig.enabled !== false) { | ||
| // Map provider ID to friendly name | ||
| const nameMap: Record<string, string> = { | ||
| google: 'Google', | ||
| github: 'GitHub', | ||
| microsoft: 'Microsoft', | ||
| apple: 'Apple', | ||
| facebook: 'Facebook', | ||
| twitter: 'Twitter', | ||
| discord: 'Discord', | ||
| gitlab: 'GitLab', | ||
| linkedin: 'LinkedIn', | ||
| }; | ||
|
|
||
| socialProviders.push({ | ||
| id, | ||
| name: nameMap[id] || id.charAt(0).toUpperCase() + id.slice(1), | ||
| enabled: true, | ||
| }); | ||
|
Comment on lines
+348
to
+371
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| // Extract email/password config (safe fields only) | ||
| const emailPasswordConfig = this.config.emailAndPassword || {}; | ||
| const emailPassword = { | ||
| enabled: emailPasswordConfig.enabled !== false, // Default to true | ||
| disableSignUp: emailPasswordConfig.disableSignUp, | ||
| requireEmailVerification: emailPasswordConfig.requireEmailVerification, | ||
| }; | ||
|
|
||
| // Extract enabled features | ||
| const pluginConfig = this.config.plugins || {}; | ||
| const features = { | ||
| twoFactor: pluginConfig.twoFactor || false, | ||
| passkeys: pluginConfig.passkeys || false, | ||
| magicLink: pluginConfig.magicLink || false, | ||
| organization: pluginConfig.organization || false, | ||
| }; | ||
|
|
||
| return { | ||
| emailPassword, | ||
| socialProviders, | ||
| features, | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -245,6 +245,28 @@ export class AuthPlugin implements Plugin { | |
|
|
||
| const rawApp = (httpServer as any).getRawApp(); | ||
|
|
||
| // Register auth config endpoint - public endpoint for frontend discovery | ||
| rawApp.get(`${basePath}/config`, async (c: any) => { | ||
| try { | ||
| const config = this.authManager!.getPublicConfig(); | ||
| return c.json({ | ||
| success: true, | ||
| data: config, | ||
| }); | ||
| } catch (error) { | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| ctx.logger.error('Auth config error:', err); | ||
|
|
||
| return c.json({ | ||
| success: false, | ||
| error: { | ||
| code: 'auth_config_error', | ||
| message: err.message, | ||
| }, | ||
| }, 500); | ||
|
Comment on lines
+256
to
+266
|
||
| } | ||
| }); | ||
|
|
||
| // Register wildcard route to forward all auth requests to better-auth. | ||
| // better-auth is configured with basePath matching our route prefix, so we | ||
| // forward the original request directly — no path rewriting needed. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -157,6 +157,53 @@ export const EndpointMapping = { | |
| '/refresh': AuthEndpointPaths.getSession, // Session refresh handled by better-auth automatically | ||
| } as const; | ||
|
|
||
| // ========================================== | ||
| // Auth Configuration Discovery | ||
| // ========================================== | ||
|
|
||
| /** | ||
| * Auth Provider Information (Public) | ||
| * | ||
| * Public-facing information about an OAuth/social provider. | ||
| * Does NOT include sensitive configuration (clientSecret, etc.) | ||
| */ | ||
| export const AuthProviderInfoSchema = z.object({ | ||
| id: z.string().describe('Provider ID (e.g., google, github, microsoft)'), | ||
| name: z.string().describe('Display name (e.g., Google, GitHub)'), | ||
| enabled: z.boolean().describe('Whether this provider is enabled'), | ||
| }); | ||
|
|
||
| /** | ||
| * Email/Password Configuration (Public) | ||
| */ | ||
| export const EmailPasswordConfigPublicSchema = z.object({ | ||
| enabled: z.boolean().describe('Whether email/password auth is enabled'), | ||
| disableSignUp: z.boolean().optional().describe('Whether new user registration is disabled'), | ||
| requireEmailVerification: z.boolean().optional().describe('Whether email verification is required'), | ||
| }); | ||
|
|
||
| /** | ||
| * Auth Features Configuration (Public) | ||
| */ | ||
| export const AuthFeaturesConfigSchema = z.object({ | ||
| twoFactor: z.boolean().default(false).describe('Two-factor authentication enabled'), | ||
| passkeys: z.boolean().default(false).describe('Passkey/WebAuthn support enabled'), | ||
| magicLink: z.boolean().default(false).describe('Magic link login enabled'), | ||
| organization: z.boolean().default(false).describe('Multi-tenant organization support enabled'), | ||
| }); | ||
|
|
||
| /** | ||
| * Get Auth Config Response | ||
| * | ||
| * Returns the public authentication configuration that the frontend | ||
| * can use to render appropriate login UI (social provider buttons, etc.) | ||
| */ | ||
| export const GetAuthConfigResponseSchema = z.object({ | ||
| emailPassword: EmailPasswordConfigPublicSchema.describe('Email/password authentication config'), | ||
| socialProviders: z.array(AuthProviderInfoSchema).describe('Available social/OAuth providers'), | ||
| features: AuthFeaturesConfigSchema.describe('Enabled authentication features'), | ||
| }); | ||
|
Comment on lines
+160
to
+205
|
||
|
|
||
| // ========================================== | ||
| // Type Exports | ||
| // ========================================== | ||
|
|
@@ -165,3 +212,7 @@ export type AuthEndpoint = z.infer<typeof AuthEndpointSchema>; | |
| export type AuthEndpointPath = typeof AuthEndpointPaths[keyof typeof AuthEndpointPaths]; | ||
| export type AuthEndpointAlias = keyof typeof AuthEndpointAliases; | ||
| export type EndpointMappingKey = keyof typeof EndpointMapping; | ||
| export type AuthProviderInfo = z.infer<typeof AuthProviderInfoSchema>; | ||
| export type EmailPasswordConfigPublic = z.infer<typeof EmailPasswordConfigPublicSchema>; | ||
| export type AuthFeaturesConfig = z.infer<typeof AuthFeaturesConfigSchema>; | ||
| export type GetAuthConfigResponse = z.infer<typeof GetAuthConfigResponseSchema>; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auth.getConfig()currently returns an untypedunwrapResponse(res)and hardcodes'/config'. Since the PR introducesGetAuthConfigResponse(and ideally an endpoint path constant in the spec), please type this asPromise<GetAuthConfigResponse>and callunwrapResponse<GetAuthConfigResponse>(res)(and use the spec’s path constant once it exists) to keep the SDK aligned with the protocol.