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

336 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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');
}