336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
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');
|
||
}
|
||
|