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

726 lines
29 KiB
TypeScript
Raw Normal View History

2025-05-28 08:23:14 +08:00
import { nanoid } from 'nanoid';
2025-05-28 12:21:03 +08:00
import type { Context } from 'hono';
2025-05-28 08:23:14 +08:00
import type {
OIDCProviderConfig,
OIDCClient,
OIDCUser,
AuthorizationCode,
AccessToken,
RefreshToken,
IDToken,
AuthorizationRequest,
TokenRequest,
TokenResponse,
OIDCError,
DiscoveryDocument,
2025-05-28 12:21:03 +08:00
PasswordValidator,
2025-05-28 08:23:14 +08:00
} 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';
2025-05-28 12:21:03 +08:00
import { PasswordAuth } from './auth';
import { OIDCErrorFactory } from './errors';
2025-05-28 08:23:14 +08:00
/**
* OIDC Provider核心类
*/
export class OIDCProvider {
2025-05-28 12:21:03 +08:00
private readonly config: OIDCProviderConfig & {
2025-05-28 08:23:14 +08:00
tokenTTL: {
accessToken: number;
refreshToken: number;
authorizationCode: number;
idToken: number;
};
responseTypes: string[];
grantTypes: string[];
scopes: string[];
claims: string[];
enablePKCE: boolean;
requirePKCE: boolean;
rotateRefreshTokens: boolean;
};
2025-05-28 12:21:03 +08:00
private readonly storage: StorageAdapter;
private readonly tokenManager: TokenManager;
private readonly jwtUtils: JWTUtils;
private readonly findUser: (userId: string) => Promise<OIDCUser | null>;
private readonly findClient: (clientId: string) => Promise<OIDCClient | null>;
private passwordAuth: PasswordAuth;
2025-05-28 08:23:14 +08:00
constructor(config: OIDCProviderConfig) {
2025-05-28 12:21:03 +08:00
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);
}
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
private normalizeConfig(config: OIDCProviderConfig) {
return {
2025-05-28 08:23:14 +08:00
...config,
tokenTTL: {
2025-05-28 12:21:03 +08:00
accessToken: 3600,
refreshToken: 30 * 24 * 3600,
authorizationCode: 600,
idToken: 3600,
...(config.tokenTTL || {}),
2025-05-28 08:23:14 +08:00
},
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,
};
2025-05-28 12:21:03 +08:00
}
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
// 统一的令牌查找和验证
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<string, string>): 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);
2025-05-28 08:23:14 +08:00
}
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<AuthorizationRequest>,
userId?: string
): Promise<{ success: true; code: string; redirectUri: string } | { success: false; error: OIDCError; redirectUri?: string }> {
try {
2025-05-28 12:21:03 +08:00
const validationResult = await this.validateAuthorizationRequest(request, userId);
if (!validationResult.success) return validationResult;
2025-05-28 08:23:14 +08:00
const code = nanoid(32);
const now = new Date();
const authCode: AuthorizationCode = {
code,
2025-05-28 12:21:03 +08:00
client_id: request.client_id!,
user_id: userId!,
2025-05-28 08:23:14 +08:00
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,
2025-05-28 12:21:03 +08:00
expires_at: new Date(now.getTime() + this.config.tokenTTL.authorizationCode * 1000),
2025-05-28 08:23:14 +08:00
created_at: now,
};
await this.tokenManager.storeAuthorizationCode(authCode);
2025-05-28 12:21:03 +08:00
return { success: true, code, redirectUri: request.redirect_uri! };
} catch (error) {
return { ...OIDCErrorFactory.serverError(request.state), redirectUri: request.redirect_uri };
}
}
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
private async validateAuthorizationRequest(
request: Partial<AuthorizationRequest>,
userId?: string
): Promise<{ success: true; client: OIDCClient } | { success: false; error: OIDCError; redirectUri?: string }> {
if (!request.client_id) {
2025-05-28 08:23:14 +08:00
return {
2025-05-28 12:21:03 +08:00
success: false,
error: { error: 'invalid_request', error_description: 'Missing client_id parameter', state: request.state },
redirectUri: request.redirect_uri,
2025-05-28 08:23:14 +08:00
};
2025-05-28 12:21:03 +08:00
}
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) {
2025-05-28 08:23:14 +08:00
return {
success: false,
2025-05-28 12:21:03 +08:00
error: { error: 'invalid_request', error_description: validation.errors.join(', '), state: request.state },
2025-05-28 08:23:14 +08:00
redirectUri: request.redirect_uri,
};
}
2025-05-28 12:21:03 +08:00
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 };
2025-05-28 08:23:14 +08:00
}
2025-05-28 12:21:03 +08:00
private validatePKCE(request: Partial<AuthorizationRequest>, 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 };
}
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
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 };
2025-05-28 08:23:14 +08:00
}
2025-05-28 12:21:03 +08:00
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 };
2025-05-28 08:23:14 +08:00
}
2025-05-28 12:21:03 +08:00
}
return null;
}
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
async handleTokenRequest(
request: Partial<TokenRequest>
): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> {
try {
const validationResult = await this.validateTokenRequest(request);
if (!validationResult.success) return validationResult;
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
const { client } = validationResult;
2025-05-28 08:23:14 +08:00
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 {
2025-05-28 12:21:03 +08:00
return OIDCErrorFactory.unsupportedGrantType();
2025-05-28 08:23:14 +08:00
}
} catch (error) {
2025-05-28 12:21:03 +08:00
return OIDCErrorFactory.createTokenError('server_error', 'Internal server error');
2025-05-28 08:23:14 +08:00
}
}
2025-05-28 12:21:03 +08:00
private async validateTokenRequest(
request: Partial<TokenRequest>
): 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 };
}
2025-05-28 08:23:14 +08:00
private async handleAuthorizationCodeGrant(
request: Partial<TokenRequest>,
client: OIDCClient
): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> {
2025-05-28 12:21:03 +08:00
if (!request.code) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing authorization code');
2025-05-28 08:23:14 +08:00
const authCode = await this.tokenManager.getAuthorizationCode(request.code);
2025-05-28 12:21:03 +08:00
if (!authCode) return OIDCErrorFactory.invalidGrant('Invalid authorization code');
2025-05-28 08:23:14 +08:00
if (authCode.expires_at < new Date()) {
await this.tokenManager.deleteAuthorizationCode(request.code);
2025-05-28 12:21:03 +08:00
return OIDCErrorFactory.invalidGrant('Authorization code expired');
2025-05-28 08:23:14 +08:00
}
2025-05-28 12:21:03 +08:00
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');
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
// PKCE验证
2025-05-28 08:23:14 +08:00
if (authCode.code_challenge) {
2025-05-28 12:21:03 +08:00
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');
2025-05-28 08:23:14 +08:00
}
}
const user = await this.findUser(authCode.user_id);
2025-05-28 12:21:03 +08:00
if (!user) return OIDCErrorFactory.invalidGrant('User not found');
2025-05-28 08:23:14 +08:00
const now = new Date();
const accessTokenJWT = await this.jwtUtils.generateAccessToken({
issuer: this.config.issuer,
subject: user.sub,
2025-05-28 12:21:03 +08:00
audience: client.client_id,
clientId: client.client_id,
2025-05-28 08:23:14 +08:00
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,
2025-05-28 12:21:03 +08:00
audience: client.client_id,
2025-05-28 08:23:14 +08:00
user,
authTime: Math.floor(now.getTime() / 1000),
nonce: authCode.nonce,
expiresIn: this.config.tokenTTL.idToken,
requestedClaims,
});
2025-05-28 12:21:03 +08:00
}
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
// 存储令牌
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({
2025-05-28 08:23:14 +08:00
token: idTokenJWT,
2025-05-28 12:21:03 +08:00
client_id: client.client_id,
2025-05-28 08:23:14 +08:00
user_id: user.sub,
2025-05-28 12:21:03 +08:00
nonce: undefined,
2025-05-28 08:23:14 +08:00
expires_at: idTokenExpiry,
created_at: now,
2025-05-28 12:21:03 +08:00
})] : [])
]);
2025-05-28 08:23:14 +08:00
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,
};
2025-05-28 12:21:03 +08:00
if (idTokenJWT) response.id_token = idTokenJWT;
return { success: true, response };
2025-05-28 08:23:14 +08:00
}
private async handleRefreshTokenGrant(
request: Partial<TokenRequest>,
client: OIDCClient
): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> {
2025-05-28 12:21:03 +08:00
if (!request.refresh_token) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing refresh_token');
2025-05-28 08:23:14 +08:00
const refreshToken = await this.tokenManager.getRefreshToken(request.refresh_token);
2025-05-28 12:21:03 +08:00
if (!refreshToken) return OIDCErrorFactory.invalidGrant('Invalid refresh_token');
2025-05-28 08:23:14 +08:00
if (refreshToken.expires_at < new Date()) {
await this.tokenManager.deleteRefreshToken(request.refresh_token);
2025-05-28 12:21:03 +08:00
return OIDCErrorFactory.invalidGrant('Refresh token expired');
2025-05-28 08:23:14 +08:00
}
2025-05-28 12:21:03 +08:00
if (refreshToken.client_id !== request.client_id) return OIDCErrorFactory.invalidGrant('Refresh token was not issued to this client');
2025-05-28 08:23:14 +08:00
const user = await this.findUser(refreshToken.user_id);
2025-05-28 12:21:03 +08:00
if (!user) return OIDCErrorFactory.invalidGrant('User not found');
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
// 处理作用域
2025-05-28 08:23:14 +08:00
let scope = refreshToken.scope;
if (request.scope) {
const requestedScopes = request.scope.split(' ');
const originalScopes = refreshToken.scope.split(' ');
if (!requestedScopes.every(s => originalScopes.includes(s))) {
2025-05-28 12:21:03 +08:00
return OIDCErrorFactory.invalidScope('Requested scope exceeds original scope');
2025-05-28 08:23:14 +08:00
}
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,
});
2025-05-28 12:21:03 +08:00
await this.tokenManager.storeAccessToken({
2025-05-28 08:23:14 +08:00
token: accessTokenJWT,
client_id: request.client_id!,
user_id: user.sub,
scope,
2025-05-28 12:21:03 +08:00
expires_at: new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000),
2025-05-28 08:23:14 +08:00
created_at: now,
2025-05-28 12:21:03 +08:00
});
2025-05-28 08:23:14 +08:00
const response: TokenResponse = {
access_token: accessTokenJWT,
token_type: 'Bearer',
expires_in: this.config.tokenTTL.accessToken,
scope,
};
2025-05-28 12:21:03 +08:00
// 刷新令牌轮换
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);
2025-05-28 08:23:14 +08:00
response.refresh_token = newRefreshToken;
}
2025-05-28 12:21:03 +08:00
return { success: true, response };
2025-05-28 08:23:14 +08:00
}
async getUserInfo(accessToken: string): Promise<{ success: true; user: Partial<OIDCUser> } | { success: false; error: OIDCError }> {
try {
const payload = await this.jwtUtils.verifyToken(accessToken);
if ((payload as any).token_type !== 'access_token') {
2025-05-28 12:21:03 +08:00
return { success: false, error: OIDCErrorFactory.invalidToken('Invalid token type') };
2025-05-28 08:23:14 +08:00
}
const tokenData = await this.tokenManager.getAccessToken(accessToken);
2025-05-28 12:21:03 +08:00
if (!tokenData) return { success: false, error: OIDCErrorFactory.invalidToken('Token not found') };
2025-05-28 08:23:14 +08:00
if (tokenData.expires_at < new Date()) {
await this.tokenManager.deleteAccessToken(accessToken);
2025-05-28 12:21:03 +08:00
return { success: false, error: OIDCErrorFactory.invalidToken('Token expired') };
2025-05-28 08:23:14 +08:00
}
const user = await this.findUser(tokenData.user_id);
2025-05-28 12:21:03 +08:00
if (!user) return { success: false, error: OIDCErrorFactory.invalidToken('User not found') };
2025-05-28 08:23:14 +08:00
const requestedClaims = this.getRequestedClaims(tokenData.scope);
const filteredUser = this.filterUserClaims(user, requestedClaims);
2025-05-28 12:21:03 +08:00
return { success: true, user: filteredUser };
2025-05-28 08:23:14 +08:00
} catch (error) {
2025-05-28 12:21:03 +08:00
return { success: false, error: OIDCErrorFactory.invalidToken() };
2025-05-28 08:23:14 +08:00
}
}
private getRequestedClaims(scope: string): string[] {
const scopes = scope.split(' ');
2025-05-28 12:21:03 +08:00
const claims = ['sub'];
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
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'],
};
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
scopes.forEach(scope => {
if (scope in scopeClaimsMap) {
claims.push(...scopeClaimsMap[scope as keyof typeof scopeClaimsMap]);
}
});
2025-05-28 08:23:14 +08:00
return claims;
}
private filterUserClaims(user: OIDCUser, requestedClaims: string[]): Partial<OIDCUser> {
2025-05-28 12:21:03 +08:00
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<OIDCUser>;
2025-05-28 08:23:14 +08:00
}
2025-05-28 12:21:03 +08:00
async revokeToken(token: string): Promise<{ success: boolean; error?: OIDCError }> {
2025-05-28 08:23:14 +08:00
try {
2025-05-28 12:21:03 +08:00
const { accessToken, refreshToken } = await this.findTokenData(token);
2025-05-28 08:23:14 +08:00
if (accessToken) {
await this.tokenManager.deleteAccessToken(token);
2025-05-28 12:21:03 +08:00
} else if (refreshToken) {
await Promise.all([
this.tokenManager.deleteRefreshToken(token),
this.tokenManager.deleteAccessTokensByUserAndClient(refreshToken.user_id, refreshToken.client_id)
]);
2025-05-28 08:23:14 +08:00
}
return { success: true };
} catch (error) {
2025-05-28 12:21:03 +08:00
return { success: false, error: OIDCErrorFactory.createSimpleError('server_error', 'Internal server error') };
2025-05-28 08:23:14 +08:00
}
}
2025-05-28 12:21:03 +08:00
async introspectToken(token: string) {
2025-05-28 08:23:14 +08:00
try {
2025-05-28 12:21:03 +08:00
const { accessToken, refreshToken, tokenData, isExpired } = await this.findTokenData(token);
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
if (!tokenData || isExpired) return { active: false };
2025-05-28 08:23:14 +08:00
2025-05-28 12:21:03 +08:00
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,
};
2025-05-28 08:23:14 +08:00
} catch (error) {
return { active: false };
}
}
async getJWKS(): Promise<{ keys: any[] }> {
return await this.jwtUtils.generateJWKS();
}
2025-05-28 12:21:03 +08:00
// HTTP处理方法
async handleLogin(c: Context): Promise<Response> {
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<Response> {
return await this.passwordAuth.logout(c);
}
async handleAuthorization(c: Context): Promise<Response> {
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<Response> {
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<Response> {
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<Response> {
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<Response> {
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);
}
}