65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
// shared/lib/auth/handle-login.ts — OIDC 登录工厂(步骤 1a 新增)
|
||
// 生成 PKCE + state + nonce → 构建 Authelia 授权 URL → 设置临时 cookie → 302 跳转
|
||
import { NextResponse } from 'next/server'
|
||
import { generatePkce, generateState, buildAuthorizeUrl, type OidcClientConfig } from './oidc'
|
||
|
||
interface HandleOidcLoginConfig {
|
||
autheliaUrl: string
|
||
clientId: string
|
||
clientSecret: string
|
||
redirectUri: string
|
||
/** 强制重新认证("使用其他账号登录",对应 OIDC prompt=login) */
|
||
switchUser?: boolean
|
||
}
|
||
|
||
const cookieOpts = {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'lax' as const,
|
||
path: '/',
|
||
maxAge: 300, // 5 分钟
|
||
} as const
|
||
|
||
/** OIDC 登录入口。生成 PKCE + nonce → 设置临时 cookie → 重定向到 Authelia */
|
||
export async function handleOidcLogin(config: HandleOidcLoginConfig): Promise<NextResponse> {
|
||
// 切换账号:通过 Authelia /logout 页面清除 authelia_session
|
||
// Authelia 4.38 不支持 /api/oidc/end_session(404),且 prompt=login&max_age=0
|
||
// 均被忽略(仍然跳转同意页面)。唯一可靠方式:先到 Authelia /logout 清除 session,
|
||
// 用户确认后再回跳本页重新发起 OIDC 授权(此时无 session → Authelia 显示登录页)
|
||
if (config.switchUser) {
|
||
// 注意:rd 参数必须用不带 ?switch=1 的 login/oidc URL,否则无限循环
|
||
const returnUrl = `${config.redirectUri.replace('/api/auth/callback', '')}/api/auth/login/oidc`
|
||
const logoutUrl = `${config.autheliaUrl}/logout?rd=${encodeURIComponent(returnUrl)}`
|
||
const response = NextResponse.redirect(logoutUrl)
|
||
response.cookies.set('tlyq_session', '', { maxAge: 0, path: '/' })
|
||
response.cookies.set('session', '', { maxAge: 0, path: '/' })
|
||
return response
|
||
}
|
||
|
||
const { codeVerifier, codeChallenge } = generatePkce()
|
||
const state = generateState()
|
||
const nonce = generateState()
|
||
|
||
const oidcConfig: OidcClientConfig = {
|
||
autheliaUrl: config.autheliaUrl,
|
||
clientId: config.clientId,
|
||
clientSecret: config.clientSecret,
|
||
redirectUri: config.redirectUri,
|
||
}
|
||
|
||
const authorizeUrl = buildAuthorizeUrl(oidcConfig, {
|
||
codeChallenge,
|
||
state,
|
||
nonce,
|
||
})
|
||
|
||
const response = NextResponse.redirect(authorizeUrl)
|
||
|
||
// OIDC 临时 cookie(5 分钟生命周期)
|
||
response.cookies.set('oidc_code_verifier', codeVerifier, cookieOpts)
|
||
response.cookies.set('oidc_state', state, cookieOpts)
|
||
response.cookies.set('oidc_nonce', nonce, cookieOpts)
|
||
|
||
return response
|
||
}
|