UNPKG

@clduab11/gemini-flow

Version:

Revolutionary AI agent swarm coordination platform with Google Services integration, multimedia processing, and production-ready monitoring. Features 8 Google AI services, quantum computing capabilities, and enterprise-grade security.

621 lines (545 loc) 17.5 kB
/** * OAuth2 Provider Implementation * * Comprehensive OAuth2 flow implementation with authorization code flow, * token exchange, refresh tokens, and PKCE support for security */ import crypto from "crypto"; import { EventEmitter } from "events"; import { Logger } from "../../utils/logger.js"; import { OAuth2Config, OAuth2Tokens, OAuth2AuthorizationRequest, OAuth2TokenRequest, OAuth2TokenResponse, OAuth2ErrorResponse, PKCECodePair, AuthProvider, AuthCredentials, AuthenticationResult, RefreshTokenResult, ValidationResult, AuthError, } from "../../types/auth.js"; /** * OAuth2 Provider with comprehensive flow support */ export class OAuth2Provider extends EventEmitter implements AuthProvider { public readonly name = "oauth2"; public readonly type = "oauth2" as const; private config: OAuth2Config; private logger: Logger; private currentPKCE?: PKCECodePair; private currentState?: string; constructor(config: OAuth2Config) { super(); this.config = config; this.logger = new Logger("OAuth2Provider"); this.validateConfig(); this.logger.info("OAuth2 Provider initialized", { clientId: config.clientId.substring(0, 8) + "...", scopes: config.scopes, pkceEnabled: config.pkceEnabled, }); } /** * Start OAuth2 authentication flow */ async authenticate(): Promise<AuthenticationResult> { try { this.logger.info("Starting OAuth2 authentication flow"); // Generate PKCE pair if enabled if (this.config.pkceEnabled) { this.currentPKCE = this.generatePKCEPair(); this.logger.debug("Generated PKCE code pair for security"); } // Generate state parameter for CSRF protection this.currentState = this.generateState(); // Build authorization URL const authUrl = this.buildAuthorizationUrl(); this.logger.info("Authorization URL generated", { url: authUrl.substring(0, 100) + "...", state: this.currentState, pkceEnabled: !!this.currentPKCE, }); // Return result with redirect URL - actual token exchange happens in exchangeCodeForTokens return { success: true, redirectUrl: authUrl, context: { sessionId: this.generateSessionId(), credentials: { type: "oauth2", provider: this.name, issuedAt: Date.now(), metadata: { state: this.currentState, pkceVerifier: this.currentPKCE?.codeVerifier, }, } as AuthCredentials, scopes: this.config.scopes, permissions: [], metadata: { flow: "authorization_code" }, createdAt: Date.now(), refreshable: true, }, }; } catch (error) { const authError = this.createAuthError( "authentication", "AUTH_FLOW_START_FAILED", "Failed to start OAuth2 authentication flow", error as Error, ); this.logger.error("Authentication flow start failed", { error: authError, }); return { success: false, error: authError }; } } /** * Exchange authorization code for access tokens */ async exchangeCodeForTokens( code: string, state?: string, codeVerifier?: string, ): Promise<AuthenticationResult> { try { this.logger.info("Exchanging authorization code for tokens"); // Validate state parameter for CSRF protection if (state && this.currentState && state !== this.currentState) { throw new Error("Invalid state parameter - potential CSRF attack"); } // Prepare token request const tokenRequest: OAuth2TokenRequest = { grantType: "authorization_code", code, redirectUri: this.config.redirectUri, clientId: this.config.clientId, clientSecret: this.config.clientSecret, }; // Add PKCE verifier if enabled if (this.config.pkceEnabled) { tokenRequest.codeVerifier = codeVerifier || this.currentPKCE?.codeVerifier; if (!tokenRequest.codeVerifier) { throw new Error("PKCE code verifier required but not provided"); } } // Exchange code for tokens const tokenResponse = await this.requestTokens(tokenRequest); // Create auth credentials const credentials: AuthCredentials = { type: "oauth2", provider: this.name, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token, expiresAt: Date.now() + tokenResponse.expires_in * 1000, scope: tokenResponse.scope ? tokenResponse.scope.split(" ") : this.config.scopes, issuedAt: Date.now(), metadata: { tokenType: tokenResponse.token_type, idToken: tokenResponse.id_token, }, }; // Create auth context const context = { sessionId: this.generateSessionId(), credentials, scopes: credentials.scope || [], permissions: await this.extractPermissions(credentials), metadata: { flow: "authorization_code", exchangeTime: Date.now(), }, createdAt: Date.now(), expiresAt: credentials.expiresAt, refreshable: !!credentials.refreshToken, }; this.logger.info("Token exchange successful", { expiresIn: tokenResponse.expires_in, hasRefreshToken: !!tokenResponse.refresh_token, scopes: credentials.scope, }); this.emit("authenticated", { credentials, context }); return { success: true, credentials, context }; } catch (error) { const authError = this.createAuthError( "authentication", "TOKEN_EXCHANGE_FAILED", "Failed to exchange authorization code for tokens", error as Error, ); this.logger.error("Token exchange failed", { error: authError }); return { success: false, error: authError }; } } /** * Refresh access tokens using refresh token */ async refresh(credentials: AuthCredentials): Promise<RefreshTokenResult> { try { if (!credentials.refreshToken) { return { success: false, requiresReauth: true, error: this.createAuthError( "authentication", "NO_REFRESH_TOKEN", "No refresh token available", ), }; } this.logger.info("Refreshing OAuth2 tokens"); const tokenRequest: OAuth2TokenRequest = { grantType: "refresh_token", refreshToken: credentials.refreshToken, clientId: this.config.clientId, clientSecret: this.config.clientSecret, scope: credentials.scope?.join(" "), }; const tokenResponse = await this.requestTokens(tokenRequest); // Update credentials const refreshedCredentials: AuthCredentials = { ...credentials, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token || credentials.refreshToken, expiresAt: Date.now() + tokenResponse.expires_in * 1000, scope: tokenResponse.scope ? tokenResponse.scope.split(" ") : credentials.scope, metadata: { ...credentials.metadata, tokenType: tokenResponse.token_type, refreshedAt: Date.now(), }, }; this.logger.info("Token refresh successful", { expiresIn: tokenResponse.expires_in, newRefreshToken: !!tokenResponse.refresh_token, }); this.emit("token_refreshed", { credentials: refreshedCredentials }); return { success: true, credentials: refreshedCredentials }; } catch (error) { const authError = this.createAuthError( "authentication", "TOKEN_REFRESH_FAILED", "Failed to refresh OAuth2 tokens", error as Error, ); this.logger.error("Token refresh failed", { error: authError }); // Check if refresh token is invalid (requires re-authentication) const requiresReauth = this.isRefreshTokenInvalid(error as Error); return { success: false, error: authError, requiresReauth, }; } } /** * Validate access token */ async validate(credentials: AuthCredentials): Promise<ValidationResult> { try { if (!credentials.accessToken) { return { valid: false, error: "No access token provided" }; } // Check expiration time const now = Date.now(); if (credentials.expiresAt && credentials.expiresAt <= now) { return { valid: false, expired: true, error: "Access token has expired", }; } // Calculate time until expiration const expiresIn = credentials.expiresAt ? Math.floor((credentials.expiresAt - now) / 1000) : undefined; // Optionally validate with userinfo endpoint if (this.config.userinfoEndpoint) { try { await this.getUserInfo(credentials.accessToken); this.logger.debug("Token validated via userinfo endpoint"); } catch (error) { this.logger.warn("Token validation via userinfo failed", { error }); return { valid: false, error: "Token validation failed at userinfo endpoint", }; } } return { valid: true, expiresIn, scopes: credentials.scope, }; } catch (error) { this.logger.error("Token validation failed", { error }); return { valid: false, error: error instanceof Error ? error.message : "Unknown validation error", }; } } /** * Revoke OAuth2 tokens */ async revoke(credentials: AuthCredentials): Promise<void> { try { if (!this.config.revokeEndpoint) { this.logger.warn( "No revoke endpoint configured, skipping token revocation", ); return; } this.logger.info("Revoking OAuth2 tokens"); const tokensToRevoke = [ credentials.accessToken, credentials.refreshToken, ].filter(Boolean) as string[]; for (const token of tokensToRevoke) { try { await this.revokeToken(token); } catch (error) { this.logger.warn("Failed to revoke token", { error }); // Continue with other tokens even if one fails } } this.logger.info("Token revocation completed"); this.emit("tokens_revoked", { credentials }); } catch (error) { this.logger.error("Token revocation failed", { error }); throw this.createAuthError( "authentication", "TOKEN_REVOCATION_FAILED", "Failed to revoke OAuth2 tokens", error as Error, ); } } /** * Generate PKCE code pair for enhanced security */ private generatePKCEPair(): PKCECodePair { // Generate code verifier (43-128 character random string) const codeVerifier = crypto.randomBytes(32).toString("base64url"); // Generate code challenge using SHA256 const codeChallenge = crypto .createHash("sha256") .update(codeVerifier) .digest("base64url"); return { codeVerifier, codeChallenge, codeChallengeMethod: "S256", }; } /** * Generate cryptographically secure state parameter */ private generateState(): string { return crypto.randomBytes(16).toString("base64url"); } /** * Generate unique session ID */ private generateSessionId(): string { return `oauth2_${Date.now()}_${crypto.randomBytes(8).toString("hex")}`; } /** * Build authorization URL with all required parameters */ private buildAuthorizationUrl(): string { const params = new URLSearchParams({ response_type: "code", client_id: this.config.clientId, redirect_uri: this.config.redirectUri, scope: this.config.scopes.join(" "), state: this.currentState!, access_type: "offline", // Request refresh token prompt: "consent", // Ensure we get refresh token }); // Add PKCE parameters if enabled if (this.currentPKCE) { params.append("code_challenge", this.currentPKCE.codeChallenge); params.append( "code_challenge_method", this.currentPKCE.codeChallengeMethod, ); } return `${this.config.authorizationEndpoint}?${params.toString()}`; } /** * Request tokens from OAuth2 token endpoint */ private async requestTokens( request: OAuth2TokenRequest, ): Promise<OAuth2TokenResponse> { const body = new URLSearchParams({ grant_type: request.grantType, client_id: request.clientId, }); // Add parameters based on grant type if (request.grantType === "authorization_code") { body.append("code", request.code!); body.append("redirect_uri", request.redirectUri!); if (request.codeVerifier) { body.append("code_verifier", request.codeVerifier); } } else if (request.grantType === "refresh_token") { body.append("refresh_token", request.refreshToken!); if (request.scope) { body.append("scope", request.scope); } } // Add client secret if provided if (request.clientSecret) { body.append("client_secret", request.clientSecret); } const response = await fetch(this.config.tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": "GeminiFlow/1.1.0 OAuth2Provider", }, body: body.toString(), }); const responseData = await response.json(); if (!response.ok) { const errorResponse = responseData as OAuth2ErrorResponse; throw new Error( `OAuth2 token request failed: ${errorResponse.error} - ${errorResponse.error_description || "Unknown error"}`, ); } return responseData as OAuth2TokenResponse; } /** * Get user information from userinfo endpoint */ private async getUserInfo(accessToken: string): Promise<any> { if (!this.config.userinfoEndpoint) { throw new Error("No userinfo endpoint configured"); } const response = await fetch(this.config.userinfoEndpoint, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", }, }); if (!response.ok) { throw new Error( `Userinfo request failed: ${response.status} ${response.statusText}`, ); } return response.json(); } /** * Revoke a token at the revoke endpoint */ private async revokeToken(token: string): Promise<void> { if (!this.config.revokeEndpoint) { return; } const body = new URLSearchParams({ token, client_id: this.config.clientId, }); if (this.config.clientSecret) { body.append("client_secret", this.config.clientSecret); } const response = await fetch(this.config.revokeEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); if (!response.ok) { throw new Error( `Token revocation failed: ${response.status} ${response.statusText}`, ); } } /** * Extract permissions from credentials (provider-specific) */ private async extractPermissions( credentials: AuthCredentials, ): Promise<string[]> { // This would be implemented based on the OAuth2 provider's scope-to-permission mapping // For now, return the scopes as permissions return credentials.scope || []; } /** * Check if refresh token is invalid and requires re-authentication */ private isRefreshTokenInvalid(error: Error): boolean { const invalidTokenErrors = [ "invalid_grant", "invalid_request", "unauthorized_client", ]; return invalidTokenErrors.some((errorType) => error.message.toLowerCase().includes(errorType), ); } /** * Validate OAuth2 configuration */ private validateConfig(): void { const required = [ "clientId", "clientSecret", "redirectUri", "authorizationEndpoint", "tokenEndpoint", ]; for (const field of required) { if (!this.config[field as keyof OAuth2Config]) { throw new Error( `OAuth2 configuration missing required field: ${field}`, ); } } if (!Array.isArray(this.config.scopes) || this.config.scopes.length === 0) { throw new Error("OAuth2 configuration must include at least one scope"); } // Validate URLs try { new URL(this.config.authorizationEndpoint); new URL(this.config.tokenEndpoint); new URL(this.config.redirectUri); } catch (error) { throw new Error("OAuth2 configuration contains invalid URLs"); } } /** * Create standardized auth error */ private createAuthError( type: AuthError["type"], code: string, message: string, originalError?: Error, ): AuthError { const error = new Error(message) as AuthError; error.code = code; error.type = type; error.retryable = type === "network" || code === "TOKEN_REFRESH_FAILED"; error.originalError = originalError; error.context = { provider: this.name, timestamp: Date.now(), }; return error; } }