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

932 lines
31 KiB
TypeScript
Raw Normal View History

2025-05-28 08:23:14 +08:00
import { nanoid } from 'nanoid';
import type {
OIDCProviderConfig,
OIDCClient,
OIDCUser,
AuthorizationCode,
AccessToken,
RefreshToken,
IDToken,
AuthorizationRequest,
TokenRequest,
TokenResponse,
OIDCError,
DiscoveryDocument,
} 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';
/**
* OIDC Provider核心类
*/
export class OIDCProvider {
private 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 storage: StorageAdapter;
private tokenManager: TokenManager;
private jwtUtils: JWTUtils;
private findUser: (userId: string) => Promise<OIDCUser | null>;
private findClient: (clientId: string) => Promise<OIDCClient | null>;
constructor(config: OIDCProviderConfig) {
// 设置默认配置
const defaultTokenTTL = {
accessToken: 3600, // 1小时
refreshToken: 30 * 24 * 3600, // 30天
authorizationCode: 600, // 10分钟
idToken: 3600, // 1小时
};
this.config = {
...config,
tokenTTL: {
...defaultTokenTTL,
...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,
};
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);
}
/**
*
*/
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 {
// 获取客户端信息
if (!request.client_id) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Missing client_id parameter',
state: request.state,
},
};
}
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,
},
};
}
// 验证请求
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,
};
}
// 强制PKCE检查
if (this.config.requirePKCE && client.client_type === 'public' && !request.code_challenge) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'PKCE is required for public clients',
state: request.state,
},
redirectUri: request.redirect_uri,
};
}
// 验证PKCE如果启用且提供了代码挑战
if (this.config.enablePKCE && request.code_challenge) {
if (!request.code_challenge_method) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Missing code_challenge_method',
state: request.state,
},
redirectUri: request.redirect_uri,
};
}
if (!PKCEUtils.isSupportedMethod(request.code_challenge_method)) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Unsupported code_challenge_method',
state: request.state,
},
redirectUri: request.redirect_uri,
};
}
if (!PKCEUtils.isValidCodeChallenge(request.code_challenge, request.code_challenge_method)) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Invalid code_challenge',
state: request.state,
},
redirectUri: request.redirect_uri,
};
}
}
// 生成授权码
const code = nanoid(32);
const now = new Date();
const expiresAt = new Date(now.getTime() + this.config.tokenTTL.authorizationCode * 1000);
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: expiresAt,
created_at: now,
};
await this.tokenManager.storeAuthorizationCode(authCode);
return {
success: true,
code,
redirectUri: request.redirect_uri!,
};
} catch (error) {
return {
success: false,
error: {
error: 'server_error',
error_description: 'Internal server error',
state: request.state,
},
redirectUri: request.redirect_uri,
};
}
}
/**
*
*/
async handleTokenRequest(
request: Partial<TokenRequest>
): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> {
try {
// 获取客户端信息
if (!request.client_id) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Missing client_id parameter',
},
};
}
const client = await this.findClient(request.client_id);
if (!client) {
return {
success: false,
error: {
error: 'invalid_client',
error_description: 'Invalid client_id',
},
};
}
// 验证客户端认证(如果需要)
if (client.client_type === 'confidential') {
if (!request.client_secret) {
return {
success: false,
error: {
error: 'invalid_client',
error_description: 'Client authentication required',
},
};
}
if (request.client_secret !== client.client_secret) {
return {
success: false,
error: {
error: 'invalid_client',
error_description: 'Invalid client credentials',
},
};
}
}
// 验证请求
const validation = ValidationUtils.validateTokenRequest(
request,
client,
this.config.grantTypes
);
if (!validation.valid) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: validation.errors.join(', '),
},
};
}
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 {
success: false,
error: {
error: 'unsupported_grant_type',
error_description: 'Grant type not supported',
},
};
}
} catch (error) {
return {
success: false,
error: {
error: 'server_error',
error_description: 'Internal server error',
},
};
}
}
/**
*
*/
private async handleAuthorizationCodeGrant(
request: Partial<TokenRequest>,
client: OIDCClient
): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> {
if (!request.code) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Missing authorization code',
},
};
}
// 获取授权码
const authCode = await this.tokenManager.getAuthorizationCode(request.code);
if (!authCode) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Invalid authorization code',
},
};
}
// 检查授权码是否过期
if (authCode.expires_at < new Date()) {
await this.tokenManager.deleteAuthorizationCode(request.code);
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Authorization code expired',
},
};
}
// 验证客户端ID
if (authCode.client_id !== request.client_id) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Authorization code was not issued to this client',
},
};
}
// 验证重定向URI
if (authCode.redirect_uri !== request.redirect_uri) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Redirect URI mismatch',
},
};
}
// 验证PKCE如果使用
if (authCode.code_challenge) {
if (!request.code_verifier) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Missing code_verifier',
},
};
}
const isValidPKCE = PKCEUtils.verifyCodeChallenge(
request.code_verifier,
authCode.code_challenge,
authCode.code_challenge_method || 'plain'
);
if (!isValidPKCE) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Invalid code_verifier',
},
};
}
}
// 获取用户信息
const user = await this.findUser(authCode.user_id);
if (!user) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'User not found',
},
};
}
// 生成令牌
const now = new Date();
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);
// 生成访问令牌
const accessTokenJWT = await this.jwtUtils.generateAccessToken({
issuer: this.config.issuer,
subject: user.sub,
audience: request.client_id!,
clientId: request.client_id!,
scope: authCode.scope,
expiresIn: this.config.tokenTTL.accessToken,
});
const accessToken: AccessToken = {
token: accessTokenJWT,
client_id: request.client_id!,
user_id: user.sub,
scope: authCode.scope,
expires_at: accessTokenExpiry,
created_at: now,
};
// 生成刷新令牌
const refreshTokenValue = nanoid(64);
const refreshToken: RefreshToken = {
token: refreshTokenValue,
client_id: request.client_id!,
user_id: user.sub,
scope: authCode.scope,
expires_at: refreshTokenExpiry,
created_at: now,
};
// 生成ID令牌如果请求了openid作用域
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: request.client_id!,
user,
authTime: Math.floor(now.getTime() / 1000),
nonce: authCode.nonce,
expiresIn: this.config.tokenTTL.idToken,
requestedClaims,
});
const idToken: IDToken = {
token: idTokenJWT,
client_id: request.client_id!,
user_id: user.sub,
nonce: authCode.nonce,
expires_at: idTokenExpiry,
created_at: now,
};
await this.tokenManager.storeIDToken(idToken);
}
// 存储令牌
await this.tokenManager.storeAccessToken(accessToken);
await this.tokenManager.storeRefreshToken(refreshToken);
// 删除已使用的授权码
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,
};
}
/**
* - OIDC规范
*/
private async handleRefreshTokenGrant(
request: Partial<TokenRequest>,
client: OIDCClient
): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> {
if (!request.refresh_token) {
return {
success: false,
error: {
error: 'invalid_request',
error_description: 'Missing refresh_token',
},
};
}
// 获取刷新令牌
const refreshToken = await this.tokenManager.getRefreshToken(request.refresh_token);
if (!refreshToken) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Invalid refresh_token',
},
};
}
// 检查刷新令牌是否过期
if (refreshToken.expires_at < new Date()) {
await this.tokenManager.deleteRefreshToken(request.refresh_token);
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Refresh token expired',
},
};
}
// 验证客户端ID
if (refreshToken.client_id !== request.client_id) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: 'Refresh token was not issued to this client',
},
};
}
// 获取用户信息
const user = await this.findUser(refreshToken.user_id);
if (!user) {
return {
success: false,
error: {
error: 'invalid_grant',
error_description: '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 {
success: false,
error: {
error: 'invalid_scope',
error_description: 'Requested scope exceeds original scope',
},
};
}
scope = request.scope;
}
// 生成新的访问令牌
const now = new Date();
const accessTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000);
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,
});
const accessToken: AccessToken = {
token: accessTokenJWT,
client_id: request.client_id!,
user_id: user.sub,
scope,
expires_at: accessTokenExpiry,
created_at: now,
};
await this.tokenManager.storeAccessToken(accessToken);
// 可选:生成新的刷新令牌(刷新令牌轮换)
let newRefreshToken: string | undefined;
if (this.config.rotateRefreshTokens !== false) { // 默认启用刷新令牌轮换
const refreshTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000);
newRefreshToken = nanoid(64);
const newRefreshTokenRecord: RefreshToken = {
token: newRefreshToken,
client_id: request.client_id!,
user_id: user.sub,
scope,
expires_at: refreshTokenExpiry,
created_at: now,
};
await this.tokenManager.storeRefreshToken(newRefreshTokenRecord);
// 删除旧的刷新令牌
await this.tokenManager.deleteRefreshToken(request.refresh_token);
}
const response: TokenResponse = {
access_token: accessTokenJWT,
token_type: 'Bearer',
expires_in: this.config.tokenTTL.accessToken,
scope,
};
if (newRefreshToken) {
response.refresh_token = newRefreshToken;
}
return {
success: true,
response,
};
}
/**
*
*/
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') {
return {
success: false,
error: {
error: 'invalid_token',
error_description: 'Invalid token type',
},
};
}
// 获取令牌数据
const tokenData = await this.tokenManager.getAccessToken(accessToken);
if (!tokenData) {
return {
success: false,
error: {
error: 'invalid_token',
error_description: 'Token not found',
},
};
}
// 检查令牌是否过期
if (tokenData.expires_at < new Date()) {
await this.tokenManager.deleteAccessToken(accessToken);
return {
success: false,
error: {
error: 'invalid_token',
error_description: 'Token expired',
},
};
}
// 获取用户信息
const user = await this.findUser(tokenData.user_id);
if (!user) {
return {
success: false,
error: {
error: 'invalid_token',
error_description: '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: {
error: 'invalid_token',
error_description: 'Invalid token',
},
};
}
}
/**
*
*/
private getRequestedClaims(scope: string): string[] {
const scopes = scope.split(' ');
const claims: string[] = ['sub']; // 总是包含sub
if (scopes.includes('profile')) {
claims.push(
'name', 'family_name', 'given_name', 'middle_name', 'nickname',
'preferred_username', 'profile', 'picture', 'website', 'gender',
'birthdate', 'zoneinfo', 'locale', 'updated_at'
);
}
if (scopes.includes('email')) {
claims.push('email', 'email_verified');
}
if (scopes.includes('phone')) {
claims.push('phone_number', 'phone_number_verified');
}
if (scopes.includes('address')) {
claims.push('address');
}
return claims;
}
/**
*
*/
private filterUserClaims(user: OIDCUser, requestedClaims: string[]): Partial<OIDCUser> {
const filtered: Partial<OIDCUser> = {};
for (const claim of requestedClaims) {
if (claim in user && user[claim as keyof OIDCUser] !== undefined) {
(filtered as any)[claim] = user[claim as keyof OIDCUser];
}
}
return filtered;
}
/**
*
*/
async revokeToken(token: string, tokenTypeHint?: string): Promise<{ success: boolean; error?: OIDCError }> {
try {
// 尝试作为访问令牌撤销
const accessToken = await this.tokenManager.getAccessToken(token);
if (accessToken) {
await this.tokenManager.deleteAccessToken(token);
return { success: true };
}
// 尝试作为刷新令牌撤销
const refreshToken = await this.tokenManager.getRefreshToken(token);
if (refreshToken) {
await this.tokenManager.deleteRefreshToken(token);
// 同时撤销相关的访问令牌
await this.tokenManager.deleteAccessTokensByUserAndClient(refreshToken.user_id, refreshToken.client_id);
return { success: true };
}
// 令牌不存在根据RFC 7009这应该返回成功
return { success: true };
} catch (error) {
return {
success: false,
error: {
error: 'server_error',
error_description: 'Internal server error',
},
};
}
}
/**
*
*/
async introspectToken(token: string): Promise<{
active: boolean;
scope?: string;
client_id?: string;
username?: string;
token_type?: string;
exp?: number;
iat?: number;
sub?: string;
aud?: string;
iss?: string;
}> {
try {
// 尝试作为访问令牌内省
const accessToken = await this.tokenManager.getAccessToken(token);
if (accessToken) {
const isExpired = accessToken.expires_at < new Date();
if (isExpired) {
await this.tokenManager.deleteAccessToken(token);
return { active: false };
}
const user = await this.findUser(accessToken.user_id);
return {
active: true,
scope: accessToken.scope,
client_id: accessToken.client_id,
username: user?.username,
token_type: 'Bearer',
exp: Math.floor(accessToken.expires_at.getTime() / 1000),
iat: Math.floor(accessToken.created_at.getTime() / 1000),
sub: accessToken.user_id,
aud: accessToken.client_id,
iss: this.config.issuer,
};
}
// 尝试作为刷新令牌内省
const refreshToken = await this.tokenManager.getRefreshToken(token);
if (refreshToken) {
const isExpired = refreshToken.expires_at < new Date();
if (isExpired) {
await this.tokenManager.deleteRefreshToken(token);
return { active: false };
}
const user = await this.findUser(refreshToken.user_id);
return {
active: true,
scope: refreshToken.scope,
client_id: refreshToken.client_id,
username: user?.username,
token_type: 'refresh_token',
exp: Math.floor(refreshToken.expires_at.getTime() / 1000),
iat: Math.floor(refreshToken.created_at.getTime() / 1000),
sub: refreshToken.user_id,
aud: refreshToken.client_id,
iss: this.config.issuer,
};
}
return { active: false };
} catch (error) {
return { active: false };
}
}
/**
* JWKS
*/
async getJWKS(): Promise<{ keys: any[] }> {
return await this.jwtUtils.generateJWKS();
}
}