import { nanoid } from 'nanoid'; import type { Context } from 'hono'; import type { OIDCProviderConfig, OIDCClient, OIDCUser, AuthorizationCode, AccessToken, RefreshToken, IDToken, AuthorizationRequest, TokenRequest, TokenResponse, OIDCError, DiscoveryDocument, PasswordValidator, } from './types'; import type { StorageAdapter } from './storage/adapter'; import { TokenManager } from './auth/token-manager'; import { JWTUtils } from './utils/jwt'; import { PKCEUtils } from './utils/pkce'; import { ValidationUtils } from './utils/validation'; import { PasswordAuth } from './auth'; import { OIDCErrorFactory } from './errors'; /** * OIDC Provider核心类 */ export class OIDCProvider { private readonly config: OIDCProviderConfig & { tokenTTL: { accessToken: number; refreshToken: number; authorizationCode: number; idToken: number; }; responseTypes: string[]; grantTypes: string[]; scopes: string[]; claims: string[]; enablePKCE: boolean; requirePKCE: boolean; rotateRefreshTokens: boolean; }; private readonly storage: StorageAdapter; private readonly tokenManager: TokenManager; private readonly jwtUtils: JWTUtils; private readonly findUser: (userId: string) => Promise; private readonly findClient: (clientId: string) => Promise; private passwordAuth: PasswordAuth; constructor(config: OIDCProviderConfig) { this.config = this.normalizeConfig(config); this.storage = config.storage; this.tokenManager = new TokenManager(config.storage); this.findUser = config.findUser; this.findClient = config.findClient; this.jwtUtils = new JWTUtils(config.signingKey, config.signingAlgorithm); this.passwordAuth = new PasswordAuth(this.config, config.authConfig.passwordValidator, config.authConfig); } private normalizeConfig(config: OIDCProviderConfig) { return { ...config, tokenTTL: { accessToken: 3600, refreshToken: 30 * 24 * 3600, authorizationCode: 600, idToken: 3600, ...(config.tokenTTL || {}), }, responseTypes: config.responseTypes || ['code', 'id_token', 'token', 'code id_token', 'code token', 'id_token token', 'code id_token token'], grantTypes: config.grantTypes || ['authorization_code', 'refresh_token', 'implicit'], scopes: config.scopes || ['openid', 'profile', 'email', 'phone', 'address'], claims: config.claims || [ 'sub', 'name', 'given_name', 'family_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'email', 'email_verified', 'gender', 'birthdate', 'zoneinfo', 'locale', 'phone_number', 'phone_number_verified', 'address', 'updated_at' ], enablePKCE: config.enablePKCE ?? true, requirePKCE: config.requirePKCE ?? false, rotateRefreshTokens: config.rotateRefreshTokens ?? true, }; } // 统一的令牌查找和验证 private async findTokenData(token: string): Promise<{ accessToken?: AccessToken | null; refreshToken?: RefreshToken | null; tokenData?: AccessToken | RefreshToken | null; isExpired: boolean; }> { const [accessToken, refreshToken] = await Promise.all([ this.tokenManager.getAccessToken(token), this.tokenManager.getRefreshToken(token) ]); const tokenData = accessToken || refreshToken; const isExpired = tokenData ? tokenData.expires_at < new Date() : false; if (isExpired && tokenData) { await Promise.all([ accessToken ? this.tokenManager.deleteAccessToken(token) : Promise.resolve(), refreshToken ? this.tokenManager.deleteRefreshToken(token) : Promise.resolve(), ]); } return { accessToken, refreshToken, tokenData, isExpired }; } private parseAuthorizationRequest(query: Record): AuthorizationRequest { return { 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, 10) : undefined, id_token_hint: query.id_token_hint, login_hint: query.login_hint, acr_values: query.acr_values, }; } private parseTokenRequest(body: FormData, authHeader?: string): TokenRequest { const tokenRequest: any = { grant_type: body.get('grant_type')?.toString() || '', client_id: body.get('client_id')?.toString() || '' }; ['code', 'redirect_uri', 'client_secret', 'refresh_token', 'code_verifier', 'scope'] .forEach(field => { const value = body.get(field)?.toString(); if (value) tokenRequest[field] = value; }); // 处理Basic认证 if (authHeader?.startsWith('Basic ')) { try { const [id, secret] = atob(authHeader.substring(6)).split(':'); if (id) tokenRequest.client_id = id; if (secret) tokenRequest.client_secret = secret; } catch { // 忽略无效的Basic认证头 } } return tokenRequest; } configurePasswordAuth(passwordValidator: PasswordValidator, authConfig: OIDCProviderConfig['authConfig']): void { this.passwordAuth = new PasswordAuth(this.config, passwordValidator, authConfig); } getDiscoveryDocument(): DiscoveryDocument { const baseUrl = this.config.issuer; return { issuer: this.config.issuer, authorization_endpoint: `${baseUrl}/auth`, token_endpoint: `${baseUrl}/token`, userinfo_endpoint: `${baseUrl}/userinfo`, jwks_uri: `${baseUrl}/.well-known/jwks.json`, registration_endpoint: `${baseUrl}/register`, revocation_endpoint: `${baseUrl}/revoke`, introspection_endpoint: `${baseUrl}/introspect`, end_session_endpoint: `${baseUrl}/logout`, response_types_supported: this.config.responseTypes, grant_types_supported: this.config.grantTypes, scopes_supported: this.config.scopes, claims_supported: this.config.claims, token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'], id_token_signing_alg_values_supported: ['HS256', 'RS256', 'ES256'], subject_types_supported: ['public'], code_challenge_methods_supported: this.config.enablePKCE ? ['plain', 'S256'] : undefined, response_modes_supported: ['query', 'fragment', 'form_post'], claims_parameter_supported: false, request_parameter_supported: false, request_uri_parameter_supported: false, require_request_uri_registration: false, }; } async handleAuthorizationRequest( request: Partial, userId?: string ): Promise<{ success: true; code: string; redirectUri: string } | { success: false; error: OIDCError; redirectUri?: string }> { try { const validationResult = await this.validateAuthorizationRequest(request, userId); if (!validationResult.success) return validationResult; const code = nanoid(32); const now = new Date(); const authCode: AuthorizationCode = { code, client_id: request.client_id!, user_id: userId!, redirect_uri: request.redirect_uri!, scope: request.scope!, code_challenge: request.code_challenge, code_challenge_method: request.code_challenge_method, nonce: request.nonce, state: request.state, expires_at: new Date(now.getTime() + this.config.tokenTTL.authorizationCode * 1000), created_at: now, }; await this.tokenManager.storeAuthorizationCode(authCode); return { success: true, code, redirectUri: request.redirect_uri! }; } catch (error) { return { ...OIDCErrorFactory.serverError(request.state), redirectUri: request.redirect_uri }; } } private async validateAuthorizationRequest( request: Partial, userId?: string ): Promise<{ success: true; client: OIDCClient } | { success: false; error: OIDCError; redirectUri?: string }> { if (!request.client_id) { return { success: false, error: { error: 'invalid_request', error_description: 'Missing client_id parameter', state: request.state }, redirectUri: request.redirect_uri, }; } const client = await this.findClient(request.client_id); if (!client) { return { success: false, error: { error: 'invalid_client', error_description: 'Invalid client_id', state: request.state }, redirectUri: request.redirect_uri, }; } const validation = ValidationUtils.validateAuthorizationRequest( request, client, this.config.scopes, this.config.responseTypes ); if (!validation.valid) { return { success: false, error: { error: 'invalid_request', error_description: validation.errors.join(', '), state: request.state }, redirectUri: request.redirect_uri, }; } if (!userId) { return { success: false, error: { error: 'login_required', error_description: 'User authentication is required', state: request.state }, redirectUri: request.redirect_uri, }; } const pkceError = this.validatePKCE(request, client); if (pkceError) return { success: false, error: pkceError, redirectUri: request.redirect_uri }; return { success: true, client }; } private validatePKCE(request: Partial, client: OIDCClient): OIDCError | null { if (this.config.requirePKCE && client.client_type === 'public' && !request.code_challenge) { return { error: 'invalid_request', error_description: 'PKCE is required for public clients', state: request.state }; } if (this.config.enablePKCE && request.code_challenge) { if (!request.code_challenge_method) { return { error: 'invalid_request', error_description: 'Missing code_challenge_method', state: request.state }; } if (!PKCEUtils.isSupportedMethod(request.code_challenge_method)) { return { error: 'invalid_request', error_description: 'Unsupported code_challenge_method', state: request.state }; } if (!PKCEUtils.isValidCodeChallenge(request.code_challenge, request.code_challenge_method)) { return { error: 'invalid_request', error_description: 'Invalid code_challenge', state: request.state }; } } return null; } async handleTokenRequest( request: Partial ): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> { try { const validationResult = await this.validateTokenRequest(request); if (!validationResult.success) return validationResult; const { client } = validationResult; if (request.grant_type === 'authorization_code') { return await this.handleAuthorizationCodeGrant(request, client); } else if (request.grant_type === 'refresh_token') { return await this.handleRefreshTokenGrant(request, client); } else { return OIDCErrorFactory.unsupportedGrantType(); } } catch (error) { return OIDCErrorFactory.createTokenError('server_error', 'Internal server error'); } } private async validateTokenRequest( request: Partial ): Promise<{ success: true; client: OIDCClient } | { success: false; error: OIDCError }> { if (!request.client_id) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing client_id parameter'); const client = await this.findClient(request.client_id); if (!client) return OIDCErrorFactory.invalidClient('Invalid client_id'); // 客户端认证 if (client.client_type === 'confidential') { if (!request.client_secret) return OIDCErrorFactory.invalidClient('Client authentication required'); if (request.client_secret !== client.client_secret) return OIDCErrorFactory.invalidClient('Invalid client credentials'); } const validation = ValidationUtils.validateTokenRequest(request, client, this.config.grantTypes); if (!validation.valid) return OIDCErrorFactory.createTokenError('invalid_request', validation.errors.join(', ')); return { success: true, client }; } private async handleAuthorizationCodeGrant( request: Partial, client: OIDCClient ): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> { if (!request.code) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing authorization code'); const authCode = await this.tokenManager.getAuthorizationCode(request.code); if (!authCode) return OIDCErrorFactory.invalidGrant('Invalid authorization code'); if (authCode.expires_at < new Date()) { await this.tokenManager.deleteAuthorizationCode(request.code); return OIDCErrorFactory.invalidGrant('Authorization code expired'); } if (authCode.client_id !== request.client_id) return OIDCErrorFactory.invalidGrant('Authorization code was not issued to this client'); if (authCode.redirect_uri !== request.redirect_uri) return OIDCErrorFactory.invalidGrant('Redirect URI mismatch'); // PKCE验证 if (authCode.code_challenge) { if (!request.code_verifier) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing code_verifier'); if (!PKCEUtils.verifyCodeChallenge(request.code_verifier, authCode.code_challenge, authCode.code_challenge_method || 'plain')) { return OIDCErrorFactory.invalidGrant('Invalid code_verifier'); } } const user = await this.findUser(authCode.user_id); if (!user) return OIDCErrorFactory.invalidGrant('User not found'); const now = new Date(); const accessTokenJWT = await this.jwtUtils.generateAccessToken({ issuer: this.config.issuer, subject: user.sub, audience: client.client_id, clientId: client.client_id, scope: authCode.scope, expiresIn: this.config.tokenTTL.accessToken, }); const refreshTokenValue = nanoid(64); let idTokenJWT: string | undefined; if (authCode.scope.includes('openid')) { const requestedClaims = this.getRequestedClaims(authCode.scope); idTokenJWT = await this.jwtUtils.generateIDToken({ issuer: this.config.issuer, subject: user.sub, audience: client.client_id, user, authTime: Math.floor(now.getTime() / 1000), nonce: authCode.nonce, expiresIn: this.config.tokenTTL.idToken, requestedClaims, }); } // 存储令牌 const accessTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000); const refreshTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000); const idTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.idToken * 1000); await Promise.all([ this.tokenManager.storeAccessToken({ token: accessTokenJWT, client_id: client.client_id, user_id: user.sub, scope: authCode.scope, expires_at: accessTokenExpiry, created_at: now, }), this.tokenManager.storeRefreshToken({ token: refreshTokenValue, client_id: client.client_id, user_id: user.sub, scope: authCode.scope, expires_at: refreshTokenExpiry, created_at: now, }), ...(idTokenJWT ? [this.tokenManager.storeIDToken({ token: idTokenJWT, client_id: client.client_id, user_id: user.sub, nonce: undefined, expires_at: idTokenExpiry, created_at: now, })] : []) ]); await this.tokenManager.deleteAuthorizationCode(request.code); const response: TokenResponse = { access_token: accessTokenJWT, token_type: 'Bearer', expires_in: this.config.tokenTTL.accessToken, refresh_token: refreshTokenValue, scope: authCode.scope, }; if (idTokenJWT) response.id_token = idTokenJWT; return { success: true, response }; } private async handleRefreshTokenGrant( request: Partial, client: OIDCClient ): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> { if (!request.refresh_token) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing refresh_token'); const refreshToken = await this.tokenManager.getRefreshToken(request.refresh_token); if (!refreshToken) return OIDCErrorFactory.invalidGrant('Invalid refresh_token'); if (refreshToken.expires_at < new Date()) { await this.tokenManager.deleteRefreshToken(request.refresh_token); return OIDCErrorFactory.invalidGrant('Refresh token expired'); } if (refreshToken.client_id !== request.client_id) return OIDCErrorFactory.invalidGrant('Refresh token was not issued to this client'); const user = await this.findUser(refreshToken.user_id); if (!user) return OIDCErrorFactory.invalidGrant('User not found'); // 处理作用域 let scope = refreshToken.scope; if (request.scope) { const requestedScopes = request.scope.split(' '); const originalScopes = refreshToken.scope.split(' '); if (!requestedScopes.every(s => originalScopes.includes(s))) { return OIDCErrorFactory.invalidScope('Requested scope exceeds original scope'); } scope = request.scope; } const now = new Date(); const accessTokenJWT = await this.jwtUtils.generateAccessToken({ issuer: this.config.issuer, subject: user.sub, audience: request.client_id!, clientId: request.client_id!, scope, expiresIn: this.config.tokenTTL.accessToken, }); await this.tokenManager.storeAccessToken({ token: accessTokenJWT, client_id: request.client_id!, user_id: user.sub, scope, expires_at: new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000), created_at: now, }); const response: TokenResponse = { access_token: accessTokenJWT, token_type: 'Bearer', expires_in: this.config.tokenTTL.accessToken, scope, }; // 刷新令牌轮换 if (this.config.rotateRefreshTokens) { const newRefreshToken = nanoid(64); await this.tokenManager.storeRefreshToken({ token: newRefreshToken, client_id: request.client_id!, user_id: user.sub, scope, expires_at: new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000), created_at: now, }); await this.tokenManager.deleteRefreshToken(request.refresh_token); response.refresh_token = newRefreshToken; } return { success: true, response }; } async getUserInfo(accessToken: string): Promise<{ success: true; user: Partial } | { success: false; error: OIDCError }> { try { const payload = await this.jwtUtils.verifyToken(accessToken); if ((payload as any).token_type !== 'access_token') { return { success: false, error: OIDCErrorFactory.invalidToken('Invalid token type') }; } const tokenData = await this.tokenManager.getAccessToken(accessToken); if (!tokenData) return { success: false, error: OIDCErrorFactory.invalidToken('Token not found') }; if (tokenData.expires_at < new Date()) { await this.tokenManager.deleteAccessToken(accessToken); return { success: false, error: OIDCErrorFactory.invalidToken('Token expired') }; } const user = await this.findUser(tokenData.user_id); if (!user) return { success: false, error: OIDCErrorFactory.invalidToken('User not found') }; const requestedClaims = this.getRequestedClaims(tokenData.scope); const filteredUser = this.filterUserClaims(user, requestedClaims); return { success: true, user: filteredUser }; } catch (error) { return { success: false, error: OIDCErrorFactory.invalidToken() }; } } private getRequestedClaims(scope: string): string[] { const scopes = scope.split(' '); const claims = ['sub']; const scopeClaimsMap = { profile: ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'], email: ['email', 'email_verified'], phone: ['phone_number', 'phone_number_verified'], address: ['address'], }; scopes.forEach(scope => { if (scope in scopeClaimsMap) { claims.push(...scopeClaimsMap[scope as keyof typeof scopeClaimsMap]); } }); return claims; } private filterUserClaims(user: OIDCUser, requestedClaims: string[]): Partial { return Object.fromEntries( requestedClaims .filter(claim => claim in user && user[claim as keyof OIDCUser] !== undefined) .map(claim => [claim, user[claim as keyof OIDCUser]]) ) as Partial; } async revokeToken(token: string): Promise<{ success: boolean; error?: OIDCError }> { try { const { accessToken, refreshToken } = await this.findTokenData(token); if (accessToken) { await this.tokenManager.deleteAccessToken(token); } else if (refreshToken) { await Promise.all([ this.tokenManager.deleteRefreshToken(token), this.tokenManager.deleteAccessTokensByUserAndClient(refreshToken.user_id, refreshToken.client_id) ]); } return { success: true }; } catch (error) { return { success: false, error: OIDCErrorFactory.createSimpleError('server_error', 'Internal server error') }; } } async introspectToken(token: string) { try { const { accessToken, refreshToken, tokenData, isExpired } = await this.findTokenData(token); if (!tokenData || isExpired) return { active: false }; const user = await this.findUser(tokenData.user_id); return { active: true, scope: tokenData.scope, client_id: tokenData.client_id, username: user?.username, token_type: accessToken ? 'Bearer' : 'refresh_token', exp: Math.floor(tokenData.expires_at.getTime() / 1000), iat: Math.floor(tokenData.created_at.getTime() / 1000), sub: tokenData.user_id, aud: tokenData.client_id, iss: this.config.issuer, }; } catch (error) { return { active: false }; } } async getJWKS(): Promise<{ keys: any[] }> { return await this.jwtUtils.generateJWKS(); } // HTTP处理方法 async handleLogin(c: Context): Promise { const formData = await c.req.formData(); const authRequest = Object.fromEntries( ['response_type', 'client_id', 'redirect_uri', 'scope', 'state', 'nonce', 'code_challenge', 'code_challenge_method'] .map(field => [field, formData.get(field)?.toString() || '']) ) as any; try { const authResult = await this.passwordAuth.authenticate(c); return authResult.success ? await this.passwordAuth.handleAuthenticationSuccess(c, authResult) : await this.passwordAuth.handleAuthenticationFailure(c, authResult, authRequest); } catch (error) { console.error('登录流程处理失败:', error); return await this.passwordAuth.handleAuthenticationFailure( c, { success: false, error: '服务器内部错误' }, authRequest ); } } async handleLogout(c: Context): Promise { return await this.passwordAuth.logout(c); } async handleAuthorization(c: Context): Promise { const authRequest = this.parseAuthorizationRequest(c.req.query()); const userId = await this.passwordAuth.getCurrentUser(c); if (!userId) { return await this.passwordAuth.handleAuthenticationRequired(c, authRequest); } const result = await this.handleAuthorizationRequest(authRequest, userId); if (!result.success) { const errorParams = OIDCErrorFactory.buildErrorResponse(result.error); const redirectUri = result.redirectUri || authRequest.redirect_uri; return redirectUri ? c.redirect(`${redirectUri}?${errorParams.toString()}`) : c.json({ error: result.error.error, error_description: result.error.error_description || 'Unknown error' }, 400); } const params = new URLSearchParams(); params.set('code', result.code); if (authRequest.state) params.set('state', authRequest.state); return c.redirect(`${result.redirectUri}?${params.toString()}`); } async handleToken(c: Context): Promise { const body = await c.req.formData(); const tokenRequest = this.parseTokenRequest(body, c.req.header('Authorization')); const result = await this.handleTokenRequest(tokenRequest); return result.success ? c.json(result.response) : c.json({ error: result.error.error, error_description: result.error.error_description || 'Unknown error' }, 400); } async handleUserInfo(c: Context): Promise { const authHeader = c.req.header('Authorization'); if (!authHeader?.startsWith('Bearer ')) { c.header('WWW-Authenticate', 'Bearer'); return c.json(OIDCErrorFactory.invalidToken('无效的访问令牌'), 401); } const result = await this.getUserInfo(authHeader.substring(7)); if (!result.success) { c.header('WWW-Authenticate', `Bearer error="${result.error.error}"`); return c.json(result.error, 401); } return c.json(result.user); } async handleRevoke(c: Context): Promise { const body = await c.req.formData(); const token = body.get('token')?.toString(); if (!token) { const error = OIDCErrorFactory.invalidRequest('缺少token参数'); return c.json({ error: error.error, error_description: error.error_description || 'Invalid request' }, 400); } const result = await this.revokeToken(token); if (!result.success && result.error) { const error = result.error as OIDCError; return c.json({ error: error.error, error_description: error.error_description || 'Unknown error' }, 400); } return c.body(null, 200); } async handleIntrospect(c: Context): Promise { const body = await c.req.formData(); const token = body.get('token')?.toString(); if (!token) { const error = OIDCErrorFactory.invalidRequest('缺少token参数'); return c.json({ error: error.error, error_description: error.error_description || 'Invalid request' }, 400); } const result = await this.introspectToken(token); return c.json(result); } }