UNPKG

@dataroadinc/setup-auth

Version:

CLI tool and programmatic API for automated OAuth setup across cloud platforms

392 lines (359 loc) 12.3 kB
import fs from "fs/promises" import { GCP_OAUTH_APPLICATION_CREDENTIALS } from "../../../utils/env-handler.js" import { SetupAuthError } from "../../../utils/error.js" import axios from "axios" import { AuthClient, GoogleAuth as AuthLibGoogleAuth, GoogleAuthOptions, } from "google-auth-library" import { GoogleAuth as GaxGoogleAuth } from "google-gax" export type GcpAuthClient = { getAccessToken(): Promise<{ token: string | null | undefined }> // Add other common methods/properties used across the codebase projectId?: string quotaProjectId?: string } // Base class for managing GCP authentication export abstract class GcpAuthenticatedIdentity { protected authLibGoogleAuth: AuthLibGoogleAuth<AuthClient> protected gaxGoogleAuth: GaxGoogleAuth protected options: GoogleAuthOptions constructor(options: GoogleAuthOptions) { this.options = options this.authLibGoogleAuth = new AuthLibGoogleAuth(options) this.gaxGoogleAuth = new GaxGoogleAuth({ ...options, }) } async getAccessToken(): Promise<string> { console.log("Getting access token...") try { const client = await this.authLibGoogleAuth.getClient() if (!client) { throw new Error("Failed to get client object from authLibGoogleAuth") } const response = await client.getAccessToken() if (!response || !response.token) { throw new Error("Failed to get access token from auth library client") } return response.token } catch (error) { throw new SetupAuthError("Failed to get access token", { cause: error }) } } abstract getCurrentUserEmail(): Promise<string> /** * Get the base GoogleAuth client from google-auth-library. * Use this for most authentication needs. */ async getAuthClient(): Promise<AuthLibGoogleAuth<AuthClient>> { return this.authLibGoogleAuth } /** * Get the base GoogleAuth client from google-gax. * Use this when working with Google Cloud client libraries that specifically need gax auth. */ async getGaxAuthClient(): Promise<GaxGoogleAuth> { return this.gaxGoogleAuth } /** * Get an auth client configured for use with Resource Manager APIs * (organizations, projects, folders, etc.) */ async getAuthClientForResourceManager(): Promise< AuthLibGoogleAuth<AuthClient> > { return this.authLibGoogleAuth } /** * Get an auth client configured for use with IAM services */ async getAuthClientForIAM(): Promise<AuthLibGoogleAuth<AuthClient>> { return this.authLibGoogleAuth } /** * Get an auth client configured for use with OAuth2 services */ async getAuthClientForOAuth2(): Promise<AuthLibGoogleAuth<AuthClient>> { return this.authLibGoogleAuth } } // Subclass for User Account Authentication (gcloud auth login) export class GcpUserAccountIdentity extends GcpAuthenticatedIdentity { constructor() { super({ scopes: [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", ], clientOptions: { universeDomain: "googleapis.com", }, }) console.log("Using user account authentication") } async getCurrentUserEmail(): Promise<string> { console.log( "Attempting to retrieve user email via User Account Identity..." ) try { const token = await this.getAccessToken() if (!token) { throw new Error("No access token available for user account") } const response = await axios.get( `https://oauth2.googleapis.com/tokeninfo?access_token=${token}` ) if (response.data && response.data.email) { console.log("User email retrieved via tokeninfo:", response.data.email) return response.data.email } throw new SetupAuthError( "Could not determine user's email from tokeninfo response." ) } catch (error: unknown) { console.warn( "Tokeninfo approach failed for User Account:", error instanceof Error ? error.message : error ) function isGaxiosReauthError(err: unknown): boolean { if (err && typeof err === "object") { if ( "response" in err && typeof err.response === "object" && err.response && "data" in err.response ) { const data = err.response.data as { error_subtype?: string error?: string error_description?: string } const descriptionIncludesReauth = data?.error_description?.includes("reauth") ?? false return ( data?.error_subtype === "invalid_rapt" || (data?.error === "invalid_grant" && descriptionIncludesReauth) ) } } return false } let rootCause: unknown = error while (rootCause instanceof SetupAuthError && rootCause.originalError) { rootCause = rootCause.originalError } if (isGaxiosReauthError(rootCause)) { throw new SetupAuthError( `Failed to retrieve user email.\n\n⚠️ Reauthentication required. Please log in again using:\n\n gcloud auth login\n`, { cause: error } ) } throw new SetupAuthError( "Failed to retrieve email for User Account. Ensure you are logged in (`gcloud auth login`) and have permissions.", { cause: error, } ) } } } // Subclass for ADC (Application Default Credentials) export class GcpAdcIdentity extends GcpAuthenticatedIdentity { constructor() { // Revert to simple super call letting the library handle discovery super({ scopes: [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", ], clientOptions: { universeDomain: "googleapis.com", }, }) console.log("Using Application Default Credentials") } async getCurrentUserEmail(): Promise<string> { // Revert log message console.log("Attempting to retrieve user email via ADC...") try { // Revert log messages inside try block const token = await this.getAccessToken() // Uses the library's default mechanism const response = await axios.get( `https://oauth2.googleapis.com/tokeninfo?access_token=${token}` ) if (response.data && response.data.email) { console.log("ADC email retrieved via tokeninfo:", response.data.email) return response.data.email } throw new SetupAuthError("Could not determine email associated with ADC.") } catch (error: unknown) { // Revert log message console.warn( "Failed to retrieve email via ADC:", error instanceof Error ? error.message : error ) function isGaxiosReauthError(err: unknown): boolean { if (err && typeof err === "object") { if ( "response" in err && typeof err.response === "object" && err.response && "data" in err.response ) { const data = err.response.data as { error_subtype?: string error?: string error_description?: string } const descriptionIncludesReauth = data?.error_description?.includes("reauth") ?? false return ( data?.error_subtype === "invalid_rapt" || (data?.error === "invalid_grant" && descriptionIncludesReauth) ) } } return false } let rootCause: unknown = error while (rootCause instanceof SetupAuthError && rootCause.originalError) { rootCause = rootCause.originalError } if (isGaxiosReauthError(rootCause)) { throw new SetupAuthError( `Failed to retrieve ADC email.\n\n⚠️ Reauthentication required for Application Default Credentials. Please log in again using:\n\n gcloud auth application-default login\n`, { cause: error } ) } throw new SetupAuthError( "Failed to retrieve email using Application Default Credentials. Ensure ADC are configured correctly (`gcloud auth application-default login`) or provide a service account key.", { cause: error } ) } } } // Subclass for Service Account Authentication export class GcpServiceAccountIdentity extends GcpAuthenticatedIdentity { constructor(keyFilePath: string) { super({ keyFile: keyFilePath, scopes: [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", ], clientOptions: { universeDomain: "googleapis.com", }, }) console.log( `Using service account authentication with key file: ${keyFilePath}` ) } async getCurrentUserEmail(): Promise<string> { console.log( "Attempting to retrieve email address of the service account..." ) try { const credentials = await this.authLibGoogleAuth.getCredentials() if (credentials.client_email) { console.log( "Service Account email from credentials:", credentials.client_email ) return credentials.client_email } else { if (this.options.keyFile && typeof this.options.keyFile === "string") { try { const keyFileContent = await fs.readFile( this.options.keyFile, "utf-8" ) const keyData = JSON.parse(keyFileContent) if (keyData.client_email) { console.log( "Service Account email from key file:", keyData.client_email ) return keyData.client_email } } catch (parseError) { console.warn( "Could not parse key file to get service account email:", parseError ) } } throw new SetupAuthError( "Could not determine Service Account email from credentials or key file." ) } } catch (error) { throw new SetupAuthError("Failed to retrieve Service Account email.", { cause: error, }) } } } export type AuthenticationType = "Service Account" | "ADC" | "User Account" export type AuthenticationContext = { forceAuthType?: AuthenticationType serviceAccountPath?: string } /** * Determines the authentication type based on the command context and environment. * NOTE: This might be less critical now that email retrieval is in specific classes. */ export function detectAuthenticationType( context?: AuthenticationContext ): AuthenticationType { if (context?.forceAuthType) { return context.forceAuthType } if ( process.env.GOOGLE_APPLICATION_CREDENTIALS || context?.serviceAccountPath ) { return "Service Account" } return "ADC" } export class GcpIdentityFactory { static createIdentity( context?: AuthenticationContext ): GcpAuthenticatedIdentity { const authType = detectAuthenticationType(context) switch (authType) { case "Service Account": return GcpIdentityFactory.createServiceAccountIdentity( context?.serviceAccountPath ) case "ADC": return GcpIdentityFactory.createAdcIdentity() case "User Account": return GcpIdentityFactory.createUserIdentity() default: throw new Error(`Unsupported authentication type detected: ${authType}`) } } static createAdcIdentity(): GcpAuthenticatedIdentity { return new GcpAdcIdentity() } static createUserIdentity(): GcpAuthenticatedIdentity { return new GcpUserAccountIdentity() } static createServiceAccountIdentity( keyFilePath: string | undefined ): GcpAuthenticatedIdentity { const keyFilePath2 = keyFilePath || process.env[GCP_OAUTH_APPLICATION_CREDENTIALS] if (!keyFilePath2) { throw new SetupAuthError( `Service account key path not found in environment variable ${GCP_OAUTH_APPLICATION_CREDENTIALS}. ` + `Please run the 'gcp-setup-service-account' command first.` ) } return new GcpServiceAccountIdentity(keyFilePath2) } }