@dataroadinc/setup-auth
Version:
CLI tool and programmatic API for automated OAuth setup across cloud platforms
392 lines (359 loc) • 12.3 kB
text/typescript
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)
}
}