import { Hono } from 'hono'; import type { Context, Next } from 'hono'; import { OIDCProvider } from '../provider'; import type { OIDCProviderConfig, AuthorizationRequest, TokenRequest } from '../types'; import { AuthManager, type PasswordValidator } from '../auth'; /** * OIDC Provider配置选项 */ export interface OIDCHonoOptions { /** OIDC Provider配置 */ config: OIDCProviderConfig; /** 密码验证器 - 用于验证用户名和密码 */ passwordValidator: PasswordValidator; /** 认证配置选项 */ authConfig?: { /** 会话TTL(秒) */ sessionTTL?: number; /** 登录页面标题 */ loginPageTitle?: string; /** 品牌名称 */ brandName?: string; /** 品牌Logo URL */ logoUrl?: string; }; } /** * 创建OIDC Provider Hono应用 */ export function createOIDCProvider(options: OIDCHonoOptions): Hono { const { config, passwordValidator, authConfig = {} } = options; // 创建认证管理器 const authManager = new AuthManager( config, passwordValidator, { sessionTTL: authConfig.sessionTTL, pageConfig: { title: authConfig.loginPageTitle, brandName: authConfig.brandName, logoUrl: authConfig.logoUrl } } ); const app = new Hono(); const provider = new OIDCProvider(config); // 登录端点 app.post('/login', async (c: Context) => { // 从表单中提取授权参数 const formData = await c.req.formData(); const authRequest = { response_type: formData.get('response_type')?.toString() || '', client_id: formData.get('client_id')?.toString() || '', redirect_uri: formData.get('redirect_uri')?.toString() || '', scope: formData.get('scope')?.toString() || '', state: formData.get('state')?.toString(), nonce: formData.get('nonce')?.toString(), code_challenge: formData.get('code_challenge')?.toString(), code_challenge_method: formData.get('code_challenge_method')?.toString() as 'plain' | 'S256' | undefined, }; return await authManager.handleLogin(c, authRequest); }); // 登出端点 app.get('/logout', async (c: Context) => { return await authManager.logout(c); }); app.post('/logout', async (c: Context) => { return await authManager.logout(c); }); // 发现文档端点 app.get('/.well-known/openid-configuration', async (c: Context) => { const discovery = provider.getDiscoveryDocument(); return c.json(discovery); }); // JWKS端点 app.get('/.well-known/jwks.json', async (c: Context) => { const jwks = await provider.getJWKS(); return c.json(jwks); }); // 授权端点 - 使用认证管理器 app.get('/auth', async (c: Context) => { const query = c.req.query(); const authRequest: AuthorizationRequest = { response_type: query.response_type || '', client_id: query.client_id || '', redirect_uri: query.redirect_uri || '', scope: query.scope || '', state: query.state, nonce: query.nonce, code_challenge: query.code_challenge, code_challenge_method: query.code_challenge_method as 'plain' | 'S256' | undefined, prompt: query.prompt, max_age: query.max_age ? parseInt(query.max_age) : undefined, id_token_hint: query.id_token_hint, login_hint: query.login_hint, acr_values: query.acr_values, }; // 检查用户认证状态 const userId = await authManager.getCurrentUser(c); if (!userId) { // 用户未认证,显示登录页面 return await authManager.handleAuthenticationRequired(c, authRequest); } // 用户已认证,处理授权请求 const result = await provider.handleAuthorizationRequest(authRequest, userId); if (!result.success) { const error = result.error; const errorParams = new URLSearchParams({ error: error.error, ...(error.error_description && { error_description: error.error_description }), ...(error.error_uri && { error_uri: error.error_uri }), ...(error.state && { state: error.state }), }); const redirectUri = result.redirectUri || query.redirect_uri; if (redirectUri) { return c.redirect(`${redirectUri}?${errorParams.toString()}`); } else { c.status(400); return c.json({ error: error.error, error_description: error.error_description, }); } } // 成功生成授权码,重定向回客户端 const params = new URLSearchParams({ code: result.code, ...(query.state && { state: query.state }), }); return c.redirect(`${result.redirectUri}?${params.toString()}`); }); // 令牌端点 app.post('/token', async (c: Context) => { const body = await c.req.formData(); // 将可选字段的类型处理为可选的而不是undefined const clientId = body.get('client_id')?.toString(); const tokenRequest: TokenRequest = { grant_type: body.get('grant_type')?.toString() || '', client_id: clientId || '', }; // 可选参数,只有存在时才添加 const code = body.get('code')?.toString(); if (code) tokenRequest.code = code; const redirectUri = body.get('redirect_uri')?.toString(); if (redirectUri) tokenRequest.redirect_uri = redirectUri; const clientSecret = body.get('client_secret')?.toString(); if (clientSecret) tokenRequest.client_secret = clientSecret; const refreshToken = body.get('refresh_token')?.toString(); if (refreshToken) tokenRequest.refresh_token = refreshToken; const codeVerifier = body.get('code_verifier')?.toString(); if (codeVerifier) tokenRequest.code_verifier = codeVerifier; const scope = body.get('scope')?.toString(); if (scope) tokenRequest.scope = scope; // 客户端认证 const authHeader = c.req.header('Authorization'); if (authHeader?.startsWith('Basic ')) { const decoded = atob(authHeader.substring(6)); const [headerClientId, headerClientSecret] = decoded.split(':'); if (headerClientId) { tokenRequest.client_id = headerClientId; } if (headerClientSecret) { tokenRequest.client_secret = headerClientSecret; } } // 请求令牌 const result = await provider.handleTokenRequest(tokenRequest); if (!result.success) { c.status(400); return c.json({ error: result.error.error, error_description: result.error.error_description, }); } return c.json(result.response); }); // 用户信息端点 app.get('/userinfo', async (c: Context) => { const authHeader = c.req.header('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { c.status(401); c.header('WWW-Authenticate', 'Bearer'); return c.json({ error: 'invalid_token', error_description: '无效的访问令牌', }); } const accessToken = authHeader.substring(7); const result = await provider.getUserInfo(accessToken); if (!result.success) { c.status(401); c.header('WWW-Authenticate', `Bearer error="${result.error.error}"`); return c.json({ error: result.error.error, error_description: result.error.error_description, }); } return c.json(result.user); }); // 令牌撤销端点 app.post('/revoke', async (c: Context) => { const body = await c.req.formData(); const token = body.get('token')?.toString() || ''; const tokenTypeHint = body.get('token_type_hint')?.toString(); const clientId = body.get('client_id')?.toString(); const clientSecret = body.get('client_secret')?.toString(); if (!token) { c.status(400); return c.json({ error: 'invalid_request', error_description: '缺少token参数', }); } // 客户端认证 let authClientId = clientId; let authClientSecret = clientSecret; const authHeader = c.req.header('Authorization'); if (authHeader?.startsWith('Basic ')) { const decoded = atob(authHeader.substring(6)); const [id, secret] = decoded.split(':'); authClientId = id; authClientSecret = secret; } // 撤销令牌 const result = await provider.revokeToken(token, tokenTypeHint); if (!result.success && result.error) { c.status(400); return c.json({ error: result.error.error, error_description: result.error.error_description, }); } // 撤销成功 c.status(200); return c.body(null); }); // 令牌内省端点 app.post('/introspect', async (c: Context) => { const body = await c.req.formData(); const token = body.get('token')?.toString() || ''; const tokenTypeHint = body.get('token_type_hint')?.toString(); const clientId = body.get('client_id')?.toString(); const clientSecret = body.get('client_secret')?.toString(); if (!token) { c.status(400); return c.json({ error: 'invalid_request', error_description: '缺少token参数', }); } // 客户端认证 let authClientId = clientId; let authClientSecret = clientSecret; const authHeader = c.req.header('Authorization'); if (authHeader?.startsWith('Basic ')) { const decoded = atob(authHeader.substring(6)); const [id, secret] = decoded.split(':'); authClientId = id; authClientSecret = secret; } // 内省令牌 const result = await provider.introspectToken(token); // 返回内省结果 return c.json(result); }); // 返回应用实例 return app; } /** * OIDC Provider中间件 */ export function oidcProvider(config: OIDCProviderConfig) { const provider = new OIDCProvider(config); return (c: Context, next: Next) => { c.set('oidc:provider', provider); return next(); }; } /** * 获取OIDC Provider实例 */ export function getOIDCProvider(c: Context): OIDCProvider { return c.get('oidc:provider'); }