feat(auth): enhance trusted admin redirect handling and add local patterns
This commit is contained in:
@@ -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' }), {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user