// 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 { // 切换账号:通过 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 }