import * as crypto from 'crypto'; import type { IAuthProvider, ITokenOptions } from './interfaces.auth.js'; import type { IAuthConfig, IAuthToken, ICredentials, TRegistryProtocol } from './interfaces.core.js'; /** * Default in-memory authentication provider. * This is the reference implementation that stores tokens in memory. * For production use, implement IAuthProvider with Redis, database, or external auth. */ export class DefaultAuthProvider implements IAuthProvider { private tokenStore: Map = new Map(); private userCredentials: Map = new Map(); // username -> password hash (mock) constructor(private config: IAuthConfig) {} /** * Initialize the auth provider */ public async init(): Promise { // Initialize token store (in-memory for now) // In production, this could be Redis or a database } // ======================================================================== // IAuthProvider Implementation // ======================================================================== /** * Authenticate user credentials */ public async authenticate(credentials: ICredentials): Promise { // Mock authentication - in production, verify against database/LDAP 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; } /** * Validate any token (NPM, Maven, OCI, PyPI, RubyGems, Composer, Cargo) */ public async validateToken( tokenString: string, protocol?: TRegistryProtocol ): Promise { // OCI uses JWT (contains dots), not UUID - check first if OCI is expected if (protocol === 'oci' || tokenString.includes('.')) { const ociToken = await this.validateOciToken(tokenString); if (ociToken && (!protocol || protocol === 'oci')) { return ociToken; } // If protocol was explicitly OCI but validation failed, return null if (protocol === 'oci') { return null; } } // UUID-based tokens: single O(1) Map lookup if (this.isValidUuid(tokenString)) { const authToken = this.tokenStore.get(tokenString); if (authToken) { // If protocol specified, verify it matches if (protocol && authToken.type !== protocol) { return null; } // Check expiration if (authToken.expiresAt && authToken.expiresAt < new Date()) { this.tokenStore.delete(tokenString); return null; } return authToken; } } return null; } /** * Create a new token for a user */ public async createToken( userId: string, protocol: TRegistryProtocol, options?: ITokenOptions ): Promise { // OCI tokens use JWT if (protocol === 'oci') { return this.createOciToken(userId, options?.scopes || ['oci:*:*:*'], options?.expiresIn || 3600); } // All other protocols use UUID tokens const token = this.generateUuid(); const scopes = options?.scopes || (options?.readonly ? [`${protocol}:*:*:read`] : [`${protocol}:*:*:*`]); const authToken: IAuthToken = { type: protocol, userId, scopes, readonly: options?.readonly, expiresAt: options?.expiresIn ? new Date(Date.now() + options.expiresIn * 1000) : undefined, metadata: { created: new Date().toISOString(), }, }; this.tokenStore.set(token, authToken); return token; } /** * Revoke a token */ public async revokeToken(token: string): Promise { this.tokenStore.delete(token); } /** * Check if token has permission for an action */ 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; } /** * List all tokens for a user */ public async listUserTokens(userId: string): Promise> { const tokens: Array<{key: string; readonly: boolean; created: string; protocol?: TRegistryProtocol}> = []; 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', protocol: authToken.type, }); } } return tokens; } // ======================================================================== // OCI JWT Token Methods // ======================================================================== /** * Create an OCI JWT token */ private 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 */ private 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; } } // ======================================================================== // Helper Methods // ======================================================================== /** * Check if a scope matches a resource and action */ 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 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 */ private hashToken(token: string): string { return `sha512-${token.substring(0, 16)}...`; } }