fenghuo/packages/oidc-provider/src/middleware/hono.ts

336 lines
11 KiB
TypeScript
Raw Normal View History

2025-05-28 08:23:14 +08:00
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');
}