dify/web/app/auth/refresh/route.ts
Coding On Star c68e5e5ed3
fix(auth): prevent open redirects in post-login flows (#38864)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
2026-07-13 10:11:36 +00:00

115 lines
3.7 KiB
TypeScript

import type { LoginRedirectTarget } from '@/utils/login-redirect'
import { IS_CLOUD_EDITION } from '@/config'
import { resolveServerConsoleApiUrl } from '@/service/server'
import { getServerLoginFallback, resolveLoginRedirectTarget } from '@/utils/login-redirect'
import { basePath } from '@/utils/var'
const REFRESH_TOKEN_PATH = '/refresh-token'
const AUTH_REFRESH_PATH = `${basePath}/auth/refresh`
const INTERNAL_PATH_PARSE_BASE = 'https://login-redirect.invalid'
function normalizeRoutePathname(pathname: string) {
let decodedPathname = pathname
for (let decodeCount = 0; decodeCount < 2; decodeCount += 1) {
try {
const nextPathname = decodeURIComponent(decodedPathname)
if (nextPathname === decodedPathname) break
decodedPathname = nextPathname
} catch {
break
}
}
const collapsedPathname = decodedPathname.replace(/\/{2,}/g, '/')
if (collapsedPathname === '/') return collapsedPathname
return collapsedPathname.replace(/\/+$/, '')
}
const addBasePathToInternalTarget = (target: LoginRedirectTarget): LoginRedirectTarget => {
if (!basePath || target.kind !== 'internal') return target
const targetUrl = new URL(target.href, INTERNAL_PATH_PARSE_BASE)
if (targetUrl.pathname === basePath || targetUrl.pathname.startsWith(`${basePath}/`))
return target
return { kind: 'internal', href: `${basePath}${target.href}` }
}
const resolveSafeRedirectTarget = (request: Request): LoginRedirectTarget => {
const requestUrl = new URL(request.url)
const redirectUrl = requestUrl.searchParams.get('redirect_url')
const fallback = getServerLoginFallback(IS_CLOUD_EDITION, basePath)
if (!redirectUrl) return fallback
const target = resolveLoginRedirectTarget(redirectUrl, {
allowSameOriginAbsolute: false,
})
if (!target) return fallback
const normalizedTarget = addBasePathToInternalTarget(target)
const targetUrl = new URL(normalizedTarget.href, INTERNAL_PATH_PARSE_BASE)
if (normalizeRoutePathname(targetUrl.pathname) === normalizeRoutePathname(AUTH_REFRESH_PATH))
return fallback
return normalizedTarget
}
const getSetCookieHeaders = (headers: Headers) => {
const getSetCookie = Reflect.get(headers, 'getSetCookie')
if (typeof getSetCookie === 'function') {
const values: unknown = getSetCookie.call(headers)
return Array.isArray(values)
? values.filter((value): value is string => typeof value === 'string')
: []
}
const setCookie = headers.get('set-cookie')
return setCookie ? [setCookie] : []
}
const createRedirectResponse = (pathname: string, setCookies: string[] = []) => {
const headers = new Headers({
'Cache-Control': 'no-store',
Location: pathname,
})
for (const cookie of setCookies) headers.append('Set-Cookie', cookie)
return new Response(null, {
status: 303,
headers,
})
}
const createSigninRedirectResponse = (redirectTarget: LoginRedirectTarget) =>
createRedirectResponse(
`${basePath}/signin?redirect_url=${encodeURIComponent(redirectTarget.href)}`,
)
export async function GET(request: Request) {
const redirectTarget = resolveSafeRedirectTarget(request)
const refreshUrl = resolveServerConsoleApiUrl(REFRESH_TOKEN_PATH)
const cookie = request.headers.get('cookie')
if (!refreshUrl || !cookie) return createSigninRedirectResponse(redirectTarget)
try {
const response = await fetch(refreshUrl, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
cookie,
}),
cache: 'no-store',
})
if (!response.ok) return createSigninRedirectResponse(redirectTarget)
return createRedirectResponse(redirectTarget.href, getSetCookieHeaders(response.headers))
} catch {
return createSigninRedirectResponse(redirectTarget)
}
}