feat(auth): add auth config discovery endpoint for dynamic SSO UI rendering#1141
feat(auth): add auth config discovery endpoint for dynamic SSO UI rendering#1141
Conversation
- Add GetAuthConfigResponse schema to auth-endpoints.zod.ts - Add getPublicConfig() method to AuthManager - Add GET /api/v1/auth/config endpoint in AuthPlugin - Add auth.getConfig() method to client SDK - Add comprehensive tests for getPublicConfig() - Export auth config types from client Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/51ee5fa2-1949-4db5-aba9-7011af6f329a Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@claude[agent] 从主分支更新最新代码 |
…to claude/add-third-party-sso-support Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Remove unused AuthManager import from auth-plugin.test.ts - Merge latest changes from main branch Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/021c7504-f94b-4009-bc35-fb44d8348988 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
已完成主分支更新和代码修复。更改包括:
Commit: 15ad086 |
There was a problem hiding this comment.
Pull request overview
Adds an authentication configuration discovery capability so frontends can render login UI dynamically based on server-side auth settings, spanning spec (protocol), server plugin (endpoint + config extraction), client SDK, and tests.
Changes:
- Added public auth config discovery schemas/types to the API spec.
- Implemented
AuthManager.getPublicConfig()and exposed it viaGET /api/v1/auth/configin the auth plugin. - Added
client.auth.getConfig()plus new exported SDK types, with tests validating safe output.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/api/auth-endpoints.zod.ts | Adds Zod schemas/types for the public auth config discovery response payload. |
| packages/plugins/plugin-auth/src/auth-plugin.ts | Registers GET {basePath}/config endpoint returning a standard JSON envelope. |
| packages/plugins/plugin-auth/src/auth-manager.ts | Implements getPublicConfig() to derive non-sensitive, frontend-safe auth configuration. |
| packages/plugins/plugin-auth/src/auth-manager.test.ts | Adds tests for provider filtering, name mapping, defaults, and sensitive data exclusion. |
| packages/client/src/index.ts | Adds auth.getConfig() and exports the new config discovery types from spec. |
| // ========================================== | ||
| // 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'), | ||
| }); |
There was a problem hiding this comment.
The protocol file adds schemas for the auth config discovery response, but it does not add the actual /config endpoint to AuthEndpointPaths / AuthEndpointSchema. This leaves the documented auth endpoint contract incomplete and forces downstream callers (e.g. the client SDK) to hardcode the path. Please add a getConfig (GET) entry and path constant (and any alias/mapping if you want it discoverable via existing helpers).
| const basePath = this.options.basePath || '/api/v1/auth'; | ||
|
|
||
| // Get raw Hono app to use native wildcard routing | ||
| // Type assertion is safe here because we explicitly require Hono server as a dependency | ||
| if (!('getRawApp' in httpServer) || typeof (httpServer as any).getRawApp !== 'function') { | ||
| ctx.logger.error('HTTP server does not support getRawApp() - wildcard routing requires Hono server'); | ||
| throw new Error( | ||
| 'AuthPlugin requires HonoServerPlugin for wildcard routing support. ' + | ||
| 'Please ensure HonoServerPlugin is loaded before AuthPlugin.' | ||
| ); | ||
| } | ||
|
|
||
| const rawApp = (httpServer as any).getRawApp(); | ||
|
|
||
| // Register auth config endpoint - public endpoint for frontend discovery | ||
| rawApp.get(`${basePath}/config`, async (c: any) => { | ||
| try { |
There was a problem hiding this comment.
basePath is concatenated directly into route strings. If a caller configures basePath with a trailing slash (e.g. /api/v1/auth/), the registered routes become //config and //*, and may also diverge from the basePath passed into better-auth. Consider normalizing once (e.g. trim a trailing /) before registering routes and before passing it to AuthManager/better-auth.
| } 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); |
There was a problem hiding this comment.
This is a public endpoint; returning err.message directly can leak internal details to unauthenticated callers. Prefer logging the full error server-side, but returning a generic client-facing message (while keeping a stable code) unless you have a vetted, user-safe error message.
| 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, | ||
| }); |
There was a problem hiding this comment.
nameMap is recreated on every loop iteration. Since this method is called per request, move the provider display-name map outside the loop (or to a module-level constant) to avoid repeated allocations, and consider typing getPublicConfig()'s return type to the spec contract (e.g. GetAuthConfigResponse) so the server implementation can’t drift from the protocol.
| getConfig: async () => { | ||
| const route = this.getRoute('auth'); | ||
| const res = await this.fetch(`${this.baseUrl}${route}/config`); | ||
| return this.unwrapResponse(res); | ||
| }, |
There was a problem hiding this comment.
auth.getConfig() currently returns an untyped unwrapResponse(res) and hardcodes '/config'. Since the PR introduces GetAuthConfigResponse (and ideally an endpoint path constant in the spec), please type this as Promise<GetAuthConfigResponse> and call unwrapResponse<GetAuthConfigResponse>(res) (and use the spec’s path constant once it exists) to keep the SDK aligned with the protocol.
Frontend applications need to discover server-side auth configuration to dynamically render appropriate login buttons (email/password, Google, GitHub, etc.) without hardcoding provider lists.
Changes
Protocol Layer (
packages/spec/src/api/auth-endpoints.zod.ts)AuthProviderInfoSchema- public OAuth provider metadata (id, name, enabled)EmailPasswordConfigPublicSchema- email/password auth settingsAuthFeaturesConfigSchema- feature flags (2FA, passkeys, magic link, organization)GetAuthConfigResponseSchema- complete config discovery responseBackend (
packages/plugins/plugin-auth/)AuthManager.getPublicConfig()- extracts safe config from internal stateGET /api/v1/auth/configendpoint inAuthPluginClient SDK (
packages/client/src/index.ts)client.auth.getConfig()methodGetAuthConfigResponse,AuthProviderInfo, etc.Tests
getPublicConfig()Usage Example
Response Format
{ "success": true, "data": { "emailPassword": { "enabled": true, "disableSignUp": false, "requireEmailVerification": false }, "socialProviders": [ { "id": "google", "name": "Google", "enabled": true }, { "id": "github", "name": "GitHub", "enabled": true } ], "features": { "twoFactor": true, "passkeys": false, "magicLink": false, "organization": true } } }