fix(auth): preserve mobile provider hints (#2281)
This commit is contained in:
@@ -31,6 +31,8 @@ export const DEFAULT_AUTH_UI_URL = 'https://accounts.airi.build/ui'
|
||||
export const SERVER_DEV_PUBLIC_URL = 'https://airi-server-dev.up.railway.app'
|
||||
export const SERVER_DEV_AUTH_UI_URL = 'https://server-dev.airi-server-auth.pages.dev/ui'
|
||||
|
||||
const FORWARDED_AUTH_UI_PROVIDERS = new Set(['google', 'github', 'steam'])
|
||||
|
||||
/** Builds a route URL below the configured standalone Auth UI base. */
|
||||
export function buildAuthUiUrl(authUiUrl: string, path: string, search = ''): string {
|
||||
const target = new URL(authUiUrl)
|
||||
@@ -72,6 +74,30 @@ export function buildAuthUiRedirectUrl(authUiUrl: string, requestUrl: string, ap
|
||||
return target.toString()
|
||||
}
|
||||
|
||||
/** Restores a trusted mobile provider hint after the OIDC plugin builds its sign-in redirect. */
|
||||
function forwardAuthUiProviderHint(requestUrl: string, response: Response): Response {
|
||||
const request = new URL(requestUrl)
|
||||
const provider = request.searchParams.get('provider')
|
||||
const location = response.headers.get('location')
|
||||
|
||||
if (response.status < 300 || response.status >= 400 || !provider || !FORWARDED_AUTH_UI_PROVIDERS.has(provider) || !location)
|
||||
return response
|
||||
|
||||
const target = new URL(location, request)
|
||||
if (target.origin !== request.origin || target.pathname !== `${SERVER_AUTH_UI_BASE_PATH}/sign-in`)
|
||||
return response
|
||||
|
||||
target.searchParams.set('provider', provider)
|
||||
|
||||
const headers = new Headers(response.headers)
|
||||
headers.set('location', location.startsWith('/') ? `${target.pathname}${target.search}${target.hash}` : target.toString())
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
const remoteJwksByUrl = new Map<string, ReturnType<typeof createRemoteJWKSet>>()
|
||||
|
||||
function readBearerToken(headers: Headers): string | null {
|
||||
@@ -254,7 +280,7 @@ export async function createAuthRoutes(deps: AuthRoutesDeps) {
|
||||
if (!(response instanceof Response))
|
||||
throw new TypeError('Expected auth handler to return a Response')
|
||||
|
||||
return response
|
||||
return forwardAuthUiProviderHint(request.url, response)
|
||||
}
|
||||
|
||||
return new Hono<HonoEnv>()
|
||||
|
||||
@@ -90,6 +90,73 @@ describe('oidc /oauth2/userinfo ban guard', () => {
|
||||
})
|
||||
|
||||
describe('auth UI routes', () => {
|
||||
it('keeps the Google provider hint on the OIDC sign-in redirect', async () => {
|
||||
const { routes, handler } = await buildRoutes({ id: 'uid_ok', email: 'ok@example.com', banned: false, banExpires: null })
|
||||
handler.mockResolvedValueOnce(new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: '/auth/sign-in?client_id=airi-stage-pocket&response_type=code' },
|
||||
}))
|
||||
|
||||
const res = await routes.request('/api/auth/oauth2/authorize?client_id=airi-stage-pocket&response_type=code&provider=google')
|
||||
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// The OIDC plugin signs only its known query fields before it redirects.
|
||||
// It removed the mobile provider hint, so Google opened the generic sign-in page.
|
||||
//
|
||||
// Before: /auth/sign-in?client_id=airi-stage-pocket&response_type=code
|
||||
// After: /auth/sign-in?client_id=airi-stage-pocket&response_type=code&provider=google
|
||||
expect(res.headers.get('location')).toBe('/auth/sign-in?client_id=airi-stage-pocket&response_type=code&provider=google')
|
||||
})
|
||||
|
||||
it('keeps the GitHub provider hint on the OIDC sign-in redirect', async () => {
|
||||
const { routes, handler } = await buildRoutes({ id: 'uid_ok', email: 'ok@example.com', banned: false, banExpires: null })
|
||||
handler.mockResolvedValueOnce(new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: '/auth/sign-in?client_id=airi-stage-pocket&response_type=code' },
|
||||
}))
|
||||
|
||||
const res = await routes.request('/api/auth/oauth2/authorize?client_id=airi-stage-pocket&response_type=code&provider=github')
|
||||
|
||||
expect(res.headers.get('location')).toBe('/auth/sign-in?client_id=airi-stage-pocket&response_type=code&provider=github')
|
||||
})
|
||||
|
||||
it('keeps the Steam provider hint on the OIDC sign-in redirect', async () => {
|
||||
const { routes, handler } = await buildRoutes({ id: 'uid_ok', email: 'ok@example.com', banned: false, banExpires: null })
|
||||
handler.mockResolvedValueOnce(new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: '/auth/sign-in?client_id=airi-stage-pocket&response_type=code' },
|
||||
}))
|
||||
|
||||
const res = await routes.request('/api/auth/oauth2/authorize?client_id=airi-stage-pocket&response_type=code&provider=steam')
|
||||
|
||||
expect(res.headers.get('location')).toBe('/auth/sign-in?client_id=airi-stage-pocket&response_type=code&provider=steam')
|
||||
})
|
||||
|
||||
it('does not forward an unknown provider to the auth UI', async () => {
|
||||
const { routes, handler } = await buildRoutes({ id: 'uid_ok', email: 'ok@example.com', banned: false, banExpires: null })
|
||||
handler.mockResolvedValueOnce(new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: '/auth/sign-in?client_id=airi-stage-pocket&response_type=code' },
|
||||
}))
|
||||
|
||||
const res = await routes.request('/api/auth/oauth2/authorize?client_id=airi-stage-pocket&response_type=code&provider=unknown')
|
||||
|
||||
expect(res.headers.get('location')).toBe('/auth/sign-in?client_id=airi-stage-pocket&response_type=code')
|
||||
})
|
||||
|
||||
it('keeps the generic sign-in redirect when the request has no provider', async () => {
|
||||
const { routes, handler } = await buildRoutes({ id: 'uid_ok', email: 'ok@example.com', banned: false, banExpires: null })
|
||||
handler.mockResolvedValueOnce(new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: '/auth/sign-in?client_id=airi-stage-pocket&response_type=code' },
|
||||
}))
|
||||
|
||||
const res = await routes.request('/api/auth/oauth2/authorize?client_id=airi-stage-pocket&response_type=code')
|
||||
|
||||
expect(res.headers.get('location')).toBe('/auth/sign-in?client_id=airi-stage-pocket&response_type=code')
|
||||
})
|
||||
|
||||
it('redirects sign-in provider shortcut to the standalone auth UI', async () => {
|
||||
const { routes } = await buildRoutes({ id: 'uid_ok', email: 'ok@example.com', banned: false, banExpires: null })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user