feat(auth): enhance trusted admin redirect handling and add local patterns

This commit is contained in:
RainbowBird
2026-06-11 22:26:39 +08:00
parent 2786393aa5
commit 8518c65aa4
4 changed files with 78 additions and 2 deletions
@@ -32,7 +32,12 @@ describe('ui-admin bootstrap context', () => {
})
it('keeps non-local standalone origins unchanged without redirect context', () => {
expect(defaultStandaloneApiServerUrl('https://admin.airi.build')).toBe('https://admin.airi.build')
expect(defaultStandaloneApiServerUrl('https://admin-preview.example')).toBe('https://admin-preview.example')
})
it('defaults known standalone admin deployments to their API origins', () => {
expect(defaultStandaloneApiServerUrl('https://admin.airi.build')).toBe('https://api.airi.build')
expect(defaultStandaloneApiServerUrl('https://server-dev.airi-server-admin.pages.dev')).toBe('https://airi-server-dev.up.railway.app')
})
it('falls back to the standalone query context when the static placeholder script is still present', () => {
@@ -11,6 +11,11 @@ const TRUSTED_STANDALONE_API_SERVER_ORIGINS = [
'https://airi-server-dev.up.railway.app',
]
const DEFAULT_API_SERVER_ORIGINS_BY_ADMIN_UI_ORIGIN = new Map([
['https://admin.airi.build', 'https://api.airi.build'],
['https://server-dev.airi-server-admin.pages.dev', 'https://airi-server-dev.up.railway.app'],
])
const TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS = [
/^http:\/\/localhost(:\d+)?$/,
/^http:\/\/127\.0\.0\.1(:\d+)?$/,
@@ -59,6 +64,9 @@ export function defaultApiServerUrl(): string {
*/
export function defaultStandaloneApiServerUrl(currentOrigin: string): string {
const origin = new URL(currentOrigin).origin
const knownApiServerOrigin = DEFAULT_API_SERVER_ORIGINS_BY_ADMIN_UI_ORIGIN.get(origin)
if (knownApiServerOrigin)
return knownApiServerOrigin
if (TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS.some(pattern => pattern.test(origin))) {
const url = new URL(origin)
@@ -76,6 +76,34 @@ describe('ui-server-auth sign-in flow helpers', () => {
})
})
it('keeps trusted standalone admin redirects on the admin origin', () => {
expect(createServerSignInContext(
'https://accounts.airi.build/ui/sign-in?redirect=https%3A%2F%2Fadmin.airi.build%2Fllm-router%3Fapi_server_url%3Dhttps%253A%252F%252Fairi-server-dev.up.railway.app',
'https://api.airi.test',
)).toEqual({
callbackURL: 'https://admin.airi.build/llm-router?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app',
requestedProvider: null,
})
expect(createServerSignInContext(
'https://accounts.airi.build/ui/sign-in?redirect=http%3A%2F%2F127.0.0.1%3A5178%2Fllm-router%3Fapi_server_url%3Dhttp%253A%252F%252F127.0.0.1%253A3000',
'https://api.airi.test',
)).toEqual({
callbackURL: 'http://127.0.0.1:5178/llm-router?api_server_url=http%3A%2F%2F127.0.0.1%3A3000',
requestedProvider: null,
})
})
it('rejects absolute redirects to untrusted origins', () => {
expect(createServerSignInContext(
'https://accounts.airi.build/ui/sign-in?redirect=https%3A%2F%2Fevil.example%2Fllm-router',
'https://api.airi.test',
)).toEqual({
callbackURL: '/',
requestedProvider: null,
})
})
it('posts the selected provider and callback URL to the social sign-in endpoint', async () => {
const fetchImpl = vi.fn<typeof fetch>(async () => {
return new Response(JSON.stringify({ url: 'https://accounts.example.test/oauth/google' }), {
+36 -1
View File
@@ -3,6 +3,18 @@ import type { OAuthProvider } from '@proj-airi/stage-ui/libs/auth'
import { extractAuthError } from './auth-fetch'
import { buildAuthUiPath } from './auth-ui-base'
const TRUSTED_ADMIN_REDIRECT_ORIGINS = [
'https://admin.airi.build',
'https://server-dev.airi-server-admin.pages.dev',
]
const TRUSTED_LOCAL_ADMIN_REDIRECT_ORIGIN_PATTERNS = [
/^http:\/\/localhost(:\d+)?$/,
/^http:\/\/127\.0\.0\.1(:\d+)?$/,
/^https:\/\/localhost(:\d+)?$/,
/^https:\/\/127\.0\.0\.1(:\d+)?$/,
]
export interface ServerSignInContext {
callbackURL: string
requestedProvider: string | null
@@ -55,7 +67,14 @@ export function createServerSignInContext(currentUrl: string, apiServerUrl: stri
}
function normalizeStandaloneRedirect(currentUrl: URL, redirect: string | null): string | null {
if (!redirect || !redirect.startsWith('/') || redirect.startsWith('//'))
if (!redirect)
return null
const trustedAdminRedirect = normalizeTrustedAdminRedirect(redirect)
if (trustedAdminRedirect)
return trustedAdminRedirect
if (!redirect.startsWith('/') || redirect.startsWith('//'))
return null
if (redirect.startsWith('/admin'))
@@ -64,6 +83,22 @@ function normalizeStandaloneRedirect(currentUrl: URL, redirect: string | null):
return `${currentUrl.origin}${buildAuthUiPath(redirect)}`
}
function normalizeTrustedAdminRedirect(redirect: string): string | null {
try {
const url = new URL(redirect)
if (TRUSTED_ADMIN_REDIRECT_ORIGINS.includes(url.origin))
return url.toString()
if (TRUSTED_LOCAL_ADMIN_REDIRECT_ORIGIN_PATTERNS.some(pattern => pattern.test(url.origin)))
return url.toString()
return null
}
catch {
return null
}
}
export async function requestSocialSignInRedirect(params: SocialSignInRedirectParams): Promise<string> {
const fetchImpl = params.fetchImpl ?? fetch
const endpoint = new URL('/api/auth/sign-in/social', params.apiServerUrl)