fix(server): auth UI callback trust (#1976)

This commit is contained in:
Lovehsigure_520
2026-06-14 17:34:43 +08:00
committed by GitHub
parent 51a44c1f54
commit 16d50433b9
2 changed files with 57 additions and 1 deletions
+14 -1
View File
@@ -10,15 +10,24 @@ function getOriginFromUrl(url: string): string | undefined {
}
const TRUSTED_EXACT_ORIGINS = [
'https://airi.moeru.ai', // Production web app
'capacitor://localhost', // Capacitor mobile (iOS)
'ai.moeru.airi-pocket://links', // Android deep link
'https://airi.moeru.ai', // Production
'https://accounts.airi.build', // Standalone auth UI
'https://server-dev.airi-server-auth.pages.dev', // Server-dev standalone auth UI
'https://admin.airi.build', // Standalone admin UI
'https://server-dev.airi-server-admin.pages.dev', // Server-dev standalone admin UI
]
// NOTICE:
// Better Auth accepts non-http(s) origins by prefix (`url.startsWith(pattern)`),
// so native deep-link schemes must not be copied from TRUSTED_EXACT_ORIGINS
// into auth callback validation. Browser auth callbacks only need web origins.
const TRUSTED_AUTH_CALLBACK_ORIGINS = TRUSTED_EXACT_ORIGINS.filter((origin) => {
const protocol = new URL(origin).protocol
return protocol === 'http:' || protocol === 'https:'
})
// NOTICE:
// Private LAN / CGNAT-style dev hosts (e.g. https://10.x:5273 from cap-vite) are NOT matched
// by regex here — list them explicitly via env `ADDITIONAL_TRUSTED_ORIGINS` (see env.ts).
@@ -149,6 +158,10 @@ export function getAuthTrustedOrigins(
origins.add(apiServerOrigin)
}
for (const origin of TRUSTED_AUTH_CALLBACK_ORIGINS) {
origins.add(origin)
}
for (const origin of env.ADDITIONAL_TRUSTED_ORIGINS) {
origins.add(origin)
}
@@ -67,6 +67,11 @@ describe('origin utils', () => {
ADDITIONAL_TRUSTED_ORIGINS: [],
}, request)).toEqual([
'https://api.airi.moeru.ai',
'https://airi.moeru.ai',
'https://accounts.airi.build',
'https://server-dev.airi-server-auth.pages.dev',
'https://admin.airi.build',
'https://server-dev.airi-server-admin.pages.dev',
'http://localhost:*',
'http://127.0.0.1:*',
'http://localhost:5173',
@@ -122,9 +127,47 @@ describe('origin utils', () => {
ADDITIONAL_TRUSTED_ORIGINS: ['https://10.0.0.129:5273'],
})).toEqual([
'https://api.airi.moeru.ai',
'https://airi.moeru.ai',
'https://accounts.airi.build',
'https://server-dev.airi-server-auth.pages.dev',
'https://admin.airi.build',
'https://server-dev.airi-server-admin.pages.dev',
'https://10.0.0.129:5273',
'http://localhost:*',
'http://127.0.0.1:*',
])
})
it('does not include native deep-link schemes in Better Auth trustedOrigins', () => {
expect(getTrustedOrigin('capacitor://localhost')).toBe('capacitor://localhost')
expect(getTrustedOrigin('ai.moeru.airi-pocket://links')).toBe('ai.moeru.airi-pocket://links')
const authOrigins = getAuthTrustedOrigins({
API_SERVER_URL: 'https://api.airi.build',
ADDITIONAL_TRUSTED_ORIGINS: [],
})
expect(authOrigins).not.toContain('capacitor://localhost')
expect(authOrigins).not.toContain('ai.moeru.airi-pocket://links')
})
// ROOT CAUSE:
//
// Email verification links carry a callbackURL query parameter that Better
// Auth validates only against its trustedOrigins list. The standalone auth
// UI sends callbackURL=https://accounts.airi.build/ui/verify-email?verified=true,
// but getAuthTrustedOrigins previously listed only API_SERVER_URL,
// additional env origins, and localhost wildcards. Clicking the email from
// a normal inbox has no usable Origin/Referer header, so request-derived
// trust could not add the auth UI origin and Better Auth returned
// INVALID_CALLBACK_URL.
//
// Before patch: auth UI callback -> not in trustedOrigins -> 403.
// After patch: built-in first-party exact origins are always present.
it('includes built-in first-party origins for email verification callbacks without request headers', () => {
expect(getAuthTrustedOrigins({
API_SERVER_URL: 'https://api.airi.build',
ADDITIONAL_TRUSTED_ORIGINS: [],
})).toContain('https://accounts.airi.build')
})
})