/** * TokenService - API token management with secure hashing */ import * as plugins from '../plugins.ts'; import type { ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts'; import { ApiToken, User } from '../models/index.ts'; import { AuditService } from './audit.service.ts'; export interface ICreateTokenOptions { userId: string; name: string; protocols: TRegistryProtocol[]; scopes: ITokenScope[]; expiresInDays?: number; createdIp?: string; } export interface ITokenValidationResult { valid: boolean; token?: ApiToken; user?: User; errorCode?: string; errorMessage?: string; } export class TokenService { private auditService: AuditService; constructor(auditService?: AuditService) { this.auditService = auditService || new AuditService({ actorType: 'system' }); } /** * Generate a new API token * Returns the raw token (only shown once) and the saved token record */ public async createToken(options: ICreateTokenOptions): Promise<{ rawToken: string; token: ApiToken }> { // Generate secure random token: srg_{64 hex chars} const randomBytes = new Uint8Array(32); crypto.getRandomValues(randomBytes); const hexToken = Array.from(randomBytes) .map((b) => b.toString(16).padStart(2, '0')) .join(''); const rawToken = `srg_${hexToken}`; // Hash the token for storage const tokenHash = await this.hashToken(rawToken); const tokenPrefix = rawToken.substring(0, 12); // "srg_" + first 8 hex chars // Create token record const token = new ApiToken(); token.id = await ApiToken.getNewId(); token.userId = options.userId; token.name = options.name; token.tokenHash = tokenHash; token.tokenPrefix = tokenPrefix; token.protocols = options.protocols; token.scopes = options.scopes; token.createdAt = new Date(); token.createdIp = options.createdIp; token.usageCount = 0; token.isRevoked = false; if (options.expiresInDays) { token.expiresAt = new Date(Date.now() + options.expiresInDays * 24 * 60 * 60 * 1000); } await token.save(); // Audit log await this.auditService.logTokenCreated(token.id, token.name); return { rawToken, token }; } /** * Validate a raw token and return the token record and user */ public async validateToken(rawToken: string, ip?: string): Promise { // Check token format if (!rawToken || !rawToken.startsWith('srg_') || rawToken.length !== 68) { return { valid: false, errorCode: 'INVALID_TOKEN_FORMAT', errorMessage: 'Invalid token format', }; } // Hash and lookup const tokenHash = await this.hashToken(rawToken); const token = await ApiToken.findByHash(tokenHash); if (!token) { return { valid: false, errorCode: 'TOKEN_NOT_FOUND', errorMessage: 'Token not found', }; } // Check validity if (!token.isValid()) { if (token.isRevoked) { return { valid: false, errorCode: 'TOKEN_REVOKED', errorMessage: 'Token has been revoked', }; } return { valid: false, errorCode: 'TOKEN_EXPIRED', errorMessage: 'Token has expired', }; } // Get user const user = await User.findById(token.userId); if (!user) { return { valid: false, errorCode: 'USER_NOT_FOUND', errorMessage: 'Token owner not found', }; } if (!user.isActive) { return { valid: false, errorCode: 'USER_INACTIVE', errorMessage: 'Token owner account is inactive', }; } // Record usage await token.recordUsage(ip); return { valid: true, token, user, }; } /** * Get all tokens for a user (without sensitive data) */ public async getUserTokens(userId: string): Promise { return await ApiToken.getUserTokens(userId); } /** * Revoke a token */ public async revokeToken(tokenId: string, reason?: string): Promise { const token = await ApiToken.getInstance({ id: tokenId }); if (!token) return false; await token.revoke(reason); await this.auditService.logTokenRevoked(token.id, token.name); return true; } /** * Revoke all tokens for a user */ public async revokeAllUserTokens(userId: string, reason?: string): Promise { const tokens = await ApiToken.getUserTokens(userId); for (const token of tokens) { await token.revoke(reason); await this.auditService.logTokenRevoked(token.id, token.name); } return tokens.length; } /** * Check if token has permission for a specific action */ public checkTokenPermission( token: ApiToken, protocol: TRegistryProtocol, organizationId?: string, repositoryId?: string, action?: string ): boolean { if (!token.hasProtocol(protocol)) return false; return token.hasScope(protocol, organizationId, repositoryId, action); } /** * Hash a token using SHA-256 */ private async hashToken(rawToken: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(rawToken); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } /** * Generate token prefix for display */ public static getTokenDisplay(tokenPrefix: string): string { return `${tokenPrefix}...`; } }