import type { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js'; import * as crypto from 'crypto'; /** * Unified authentication manager for all registry protocols * Handles both NPM UUID tokens and OCI JWT tokens */ export class AuthManager { private tokenStore: Map = new Map(); private userCredentials: Map = new Map(); // username -> password hash (mock) constructor(private config: IAuthConfig) {} /** * Initialize the auth manager */ public async init(): Promise { // Initialize token store (in-memory for now) // In production, this could be Redis or a database } // ======================================================================== // UUID TOKEN CREATION (Base method for NPM, Maven, etc.) // ======================================================================== /** * Create a UUID-based token with custom scopes (base method) * @param userId - User ID * @param protocol - Protocol type * @param scopes - Permission scopes * @param readonly - Whether the token is readonly * @returns UUID token string */ private async createUuidToken( userId: string, protocol: TRegistryProtocol, scopes: string[], readonly: boolean = false ): Promise { const token = this.generateUuid(); const authToken: IAuthToken = { type: protocol, userId, scopes, readonly, metadata: { created: new Date().toISOString(), }, }; this.tokenStore.set(token, authToken); return token; } // ======================================================================== // NPM AUTHENTICATION // ======================================================================== /** * Create an NPM token * @param userId - User ID * @param readonly - Whether the token is readonly * @returns NPM UUID token */ public async createNpmToken(userId: string, readonly: boolean = false): Promise { if (!this.config.npmTokens.enabled) { throw new Error('NPM tokens are not enabled'); } const scopes = readonly ? ['npm:*:*:read'] : ['npm:*:*:*']; return this.createUuidToken(userId, 'npm', scopes, readonly); } /** * Validate an NPM token * @param token - NPM UUID token * @returns Auth token object or null */ public async validateNpmToken(token: string): Promise { if (!this.isValidUuid(token)) { return null; } const authToken = this.tokenStore.get(token); if (!authToken || authToken.type !== 'npm') { return null; } // Check expiration if set if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(token); return null; } return authToken; } /** * Revoke an NPM token * @param token - NPM UUID token */ public async revokeNpmToken(token: string): Promise { this.tokenStore.delete(token); } /** * List all tokens for a user * @param userId - User ID * @returns List of token info (without actual token values) */ public async listUserTokens(userId: string): Promise> { const tokens: Array<{key: string; readonly: boolean; created: string}> = []; for (const [token, authToken] of this.tokenStore.entries()) { if (authToken.userId === userId) { tokens.push({ key: this.hashToken(token), readonly: authToken.readonly || false, created: authToken.metadata?.created || 'unknown', }); } } return tokens; } // ======================================================================== // OCI AUTHENTICATION (JWT) // ======================================================================== /** * Create an OCI JWT token * @param userId - User ID * @param scopes - Permission scopes * @param expiresIn - Expiration time in seconds * @returns JWT token string (HMAC-SHA256 signed) */ public async createOciToken( userId: string, scopes: string[], expiresIn: number = 3600 ): Promise { if (!this.config.ociTokens.enabled) { throw new Error('OCI tokens are not enabled'); } const now = Math.floor(Date.now() / 1000); const payload = { iss: this.config.ociTokens.realm, sub: userId, aud: this.config.ociTokens.service, exp: now + expiresIn, nbf: now, iat: now, access: this.scopesToOciAccess(scopes), }; // Create JWT with HMAC-SHA256 signature const header = { alg: 'HS256', typ: 'JWT' }; const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url'); const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url'); const signature = crypto .createHmac('sha256', this.config.jwtSecret) .update(`${headerB64}.${payloadB64}`) .digest('base64url'); return `${headerB64}.${payloadB64}.${signature}`; } /** * Validate an OCI JWT token * @param jwt - JWT token string * @returns Auth token object or null */ public async validateOciToken(jwt: string): Promise { try { const parts = jwt.split('.'); if (parts.length !== 3) { return null; } const [headerB64, payloadB64, signatureB64] = parts; // Verify signature const expectedSignature = crypto .createHmac('sha256', this.config.jwtSecret) .update(`${headerB64}.${payloadB64}`) .digest('base64url'); if (signatureB64 !== expectedSignature) { return null; } // Decode and parse payload const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf-8')); // Check expiration const now = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp < now) { return null; } // Check not-before time if (payload.nbf && payload.nbf > now) { return null; } // Convert to unified token format const scopes = this.ociAccessToScopes(payload.access || []); return { type: 'oci', userId: payload.sub, scopes, expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined, metadata: { iss: payload.iss, aud: payload.aud, }, }; } catch (error) { return null; } } // ======================================================================== // UNIFIED AUTHENTICATION // ======================================================================== /** * Authenticate user credentials * @param credentials - Username and password * @returns User ID or null */ public async authenticate(credentials: ICredentials): Promise { // Mock authentication - in production, verify against database const storedPassword = this.userCredentials.get(credentials.username); if (!storedPassword) { // Auto-register for testing (remove in production) this.userCredentials.set(credentials.username, credentials.password); return credentials.username; } if (storedPassword === credentials.password) { return credentials.username; } return null; } // ======================================================================== // MAVEN AUTHENTICATION // ======================================================================== /** * Create a Maven token * @param userId - User ID * @param readonly - Whether the token is readonly * @returns Maven UUID token */ public async createMavenToken(userId: string, readonly: boolean = false): Promise { const scopes = readonly ? ['maven:*:*:read'] : ['maven:*:*:*']; return this.createUuidToken(userId, 'maven', scopes, readonly); } /** * Validate a Maven token * @param token - Maven UUID token * @returns Auth token object or null */ public async validateMavenToken(token: string): Promise { if (!this.isValidUuid(token)) { return null; } const authToken = this.tokenStore.get(token); if (!authToken || authToken.type !== 'maven') { return null; } // Check expiration if set if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(token); return null; } return authToken; } /** * Revoke a Maven token * @param token - Maven UUID token */ public async revokeMavenToken(token: string): Promise { this.tokenStore.delete(token); } // ======================================================================== // COMPOSER TOKEN MANAGEMENT // ======================================================================== /** * Create a Composer token * @param userId - User ID * @param readonly - Whether the token is readonly * @returns Composer UUID token */ public async createComposerToken(userId: string, readonly: boolean = false): Promise { const scopes = readonly ? ['composer:*:*:read'] : ['composer:*:*:*']; return this.createUuidToken(userId, 'composer', scopes, readonly); } /** * Validate a Composer token * @param token - Composer UUID token * @returns Auth token object or null */ public async validateComposerToken(token: string): Promise { if (!this.isValidUuid(token)) { return null; } const authToken = this.tokenStore.get(token); if (!authToken || authToken.type !== 'composer') { return null; } // Check expiration if set if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(token); return null; } return authToken; } /** * Revoke a Composer token * @param token - Composer UUID token */ public async revokeComposerToken(token: string): Promise { this.tokenStore.delete(token); } // ======================================================================== // CARGO TOKEN MANAGEMENT // ======================================================================== /** * Create a Cargo token * @param userId - User ID * @param readonly - Whether the token is readonly * @returns Cargo UUID token */ public async createCargoToken(userId: string, readonly: boolean = false): Promise { const scopes = readonly ? ['cargo:*:*:read'] : ['cargo:*:*:*']; return this.createUuidToken(userId, 'cargo', scopes, readonly); } /** * Validate a Cargo token * @param token - Cargo UUID token * @returns Auth token object or null */ public async validateCargoToken(token: string): Promise { if (!this.isValidUuid(token)) { return null; } const authToken = this.tokenStore.get(token); if (!authToken || authToken.type !== 'cargo') { return null; } // Check expiration if set if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(token); return null; } return authToken; } /** * Revoke a Cargo token * @param token - Cargo UUID token */ public async revokeCargoToken(token: string): Promise { this.tokenStore.delete(token); } // ======================================================================== // PYPI AUTHENTICATION // ======================================================================== /** * Create a PyPI token * @param userId - User ID * @param readonly - Whether the token is readonly * @returns PyPI UUID token */ public async createPypiToken(userId: string, readonly: boolean = false): Promise { const scopes = readonly ? ['pypi:*:*:read'] : ['pypi:*:*:*']; return this.createUuidToken(userId, 'pypi', scopes, readonly); } /** * Validate a PyPI token * @param token - PyPI UUID token * @returns Auth token object or null */ public async validatePypiToken(token: string): Promise { if (!this.isValidUuid(token)) { return null; } const authToken = this.tokenStore.get(token); if (!authToken || authToken.type !== 'pypi') { return null; } // Check expiration if set if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(token); return null; } return authToken; } /** * Revoke a PyPI token * @param token - PyPI UUID token */ public async revokePypiToken(token: string): Promise { this.tokenStore.delete(token); } // ======================================================================== // RUBYGEMS AUTHENTICATION // ======================================================================== /** * Create a RubyGems token * @param userId - User ID * @param readonly - Whether the token is readonly * @returns RubyGems UUID token */ public async createRubyGemsToken(userId: string, readonly: boolean = false): Promise { const scopes = readonly ? ['rubygems:*:*:read'] : ['rubygems:*:*:*']; return this.createUuidToken(userId, 'rubygems', scopes, readonly); } /** * Validate a RubyGems token * @param token - RubyGems UUID token * @returns Auth token object or null */ public async validateRubyGemsToken(token: string): Promise { if (!this.isValidUuid(token)) { return null; } const authToken = this.tokenStore.get(token); if (!authToken || authToken.type !== 'rubygems') { return null; } // Check expiration if set if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(token); return null; } return authToken; } /** * Revoke a RubyGems token * @param token - RubyGems UUID token */ public async revokeRubyGemsToken(token: string): Promise { this.tokenStore.delete(token); } // ======================================================================== // UNIFIED AUTHENTICATION // ======================================================================== /** * Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo) * @param tokenString - Token string (UUID or JWT) * @param protocol - Expected protocol type * @returns Auth token object or null */ public async validateToken( tokenString: string, protocol?: TRegistryProtocol ): Promise { // Try UUID-based tokens (NPM, Maven, Composer, Cargo, PyPI, RubyGems) if (this.isValidUuid(tokenString)) { // Try NPM token const npmToken = await this.validateNpmToken(tokenString); if (npmToken && (!protocol || protocol === 'npm')) { return npmToken; } // Try Maven token const mavenToken = await this.validateMavenToken(tokenString); if (mavenToken && (!protocol || protocol === 'maven')) { return mavenToken; } // Try Composer token const composerToken = await this.validateComposerToken(tokenString); if (composerToken && (!protocol || protocol === 'composer')) { return composerToken; } // Try Cargo token const cargoToken = await this.validateCargoToken(tokenString); if (cargoToken && (!protocol || protocol === 'cargo')) { return cargoToken; } // Try PyPI token const pypiToken = await this.validatePypiToken(tokenString); if (pypiToken && (!protocol || protocol === 'pypi')) { return pypiToken; } // Try RubyGems token const rubygemsToken = await this.validateRubyGemsToken(tokenString); if (rubygemsToken && (!protocol || protocol === 'rubygems')) { return rubygemsToken; } } // Try OCI JWT const ociToken = await this.validateOciToken(tokenString); if (ociToken && (!protocol || protocol === 'oci')) { return ociToken; } return null; } /** * Check if token has permission for an action * @param token - Auth token * @param resource - Resource being accessed (e.g., "package:foo" or "repository:bar") * @param action - Action being performed (read, write, push, pull, delete) * @returns true if authorized */ public async authorize( token: IAuthToken | null, resource: string, action: string ): Promise { if (!token) { return false; } // Check readonly flag if (token.readonly && ['write', 'push', 'delete'].includes(action)) { return false; } // Check scopes for (const scope of token.scopes) { if (this.matchesScope(scope, resource, action)) { return true; } } return false; } // ======================================================================== // HELPER METHODS // ======================================================================== /** * Check if a scope matches a resource and action * Scope format: "{protocol}:{type}:{name}:{action}" * Examples: * - "npm:*:*" - All NPM access * - "npm:package:foo:*" - All actions on package foo * - "npm:package:foo:read" - Read-only on package foo * - "oci:repository:*:pull" - Pull from any OCI repo */ private matchesScope(scope: string, resource: string, action: string): boolean { const scopeParts = scope.split(':'); const resourceParts = resource.split(':'); // Scope must have at least protocol:type:name:action if (scopeParts.length < 4) { return false; } const [scopeProtocol, scopeType, scopeName, scopeAction] = scopeParts; const [resourceProtocol, resourceType, resourceName] = resourceParts; // Check protocol if (scopeProtocol !== '*' && scopeProtocol !== resourceProtocol) { return false; } // Check type if (scopeType !== '*' && scopeType !== resourceType) { return false; } // Check name if (scopeName !== '*' && scopeName !== resourceName) { return false; } // Check action if (scopeAction !== '*' && scopeAction !== action) { // Map action aliases const actionAliases: Record = { read: ['pull', 'get'], write: ['push', 'put', 'post'], }; const aliases = actionAliases[scopeAction] || []; if (!aliases.includes(action)) { return false; } } return true; } /** * Convert unified scopes to OCI access array */ private scopesToOciAccess(scopes: string[]): Array<{ type: string; name: string; actions: string[]; }> { const access: Array<{type: string; name: string; actions: string[]}> = []; for (const scope of scopes) { const parts = scope.split(':'); if (parts.length >= 4 && parts[0] === 'oci') { access.push({ type: parts[1], name: parts[2], actions: [parts[3]], }); } } return access; } /** * Convert OCI access array to unified scopes */ private ociAccessToScopes(access: Array<{ type: string; name: string; actions: string[]; }>): string[] { const scopes: string[] = []; for (const item of access) { for (const action of item.actions) { scopes.push(`oci:${item.type}:${item.name}:${action}`); } } return scopes; } /** * Generate UUID for NPM tokens */ private generateUuid(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } /** * Check if string is a valid UUID */ private isValidUuid(str: string): boolean { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(str); } /** * Hash a token for identification (SHA-512 mock) */ private hashToken(token: string): string { // In production, use actual SHA-512 return `sha512-${token.substring(0, 16)}...`; } }