Skip to content
Open
79 changes: 79 additions & 0 deletions core/src/tools/openapi_tool/auth/auth_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {OpenAPIV3} from 'openapi-types';
import {AuthCredential} from '../../../auth/auth_credential.js';

/**
* Applies the given credential to the request headers and URL.
*
* @param url The target URL.
* @param headers The request headers.
* @param credential The auth credential.
* @param authScheme The auth scheme from OpenAPI spec.
* @returns The updated URL (if modified by query params).
*/
export function applyCredential(
url: string,
headers: Record<string, string>,
credential?: AuthCredential,
authScheme?: OpenAPIV3.SecuritySchemeObject,
): string {
if (!credential) return url;

if (credential.apiKey) {
let inLocation: string | undefined;
let name = 'key';

if (authScheme && authScheme.type === 'apiKey') {
const apiKeyScheme = authScheme as OpenAPIV3.ApiKeySecurityScheme;
inLocation = apiKeyScheme.in;
name = apiKeyScheme.name;
}

if (inLocation === 'header') {
headers[name] = credential.apiKey;
} else if (inLocation === 'query') {
const separator = url.includes('?') ? '&' : '?';
url += `${separator}${name}=${encodeURIComponent(credential.apiKey)}`;
} else {
// Default to header Authorization if not specified or unknown location
headers['Authorization'] = credential.apiKey;
}
} else if (
credential.http &&
credential.http.credentials &&
credential.http.credentials.token
) {
headers['Authorization'] = `Bearer ${credential.http.credentials.token}`;
}

return url;
}

/**
* Helper to create a simple API Key auth scheme.
*/
export function createApiKeyScheme(
name: string,
inLocation: 'header' | 'query' | 'cookie',
): OpenAPIV3.SecuritySchemeObject {
return {
type: 'apiKey',
name,
in: inLocation,
};
}

/**
* Helper to create a simple Bearer Token auth scheme.
*/
export function createBearerScheme(): OpenAPIV3.SecuritySchemeObject {
return {
type: 'http',
scheme: 'bearer',
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
AuthCredential,
AuthCredentialTypes,
} from '../../../../auth/auth_credential.js';
import {AuthScheme} from '../../../../auth/auth_schemes.js';
import {
BaseCredentialExchanger,
ExchangeResult,
} from '../../../../auth/exchanger/base_credential_exchanger.js';
import {OAuth2CredentialExchanger} from '../../../../auth/oauth2/oauth2_credential_exchanger.js';
import {experimental} from '../../../../utils/experimental.js';
import {ServiceAccountCredentialExchanger} from './service_account_exchanger.js';

/**
* Automatically selects the appropriate credential exchanger based on the auth scheme.
* Ported from Python implementation.
*/
@experimental
export class AutoAuthCredentialExchanger implements BaseCredentialExchanger {
private exchangers: Map<AuthCredentialTypes, BaseCredentialExchanger> =
new Map();

constructor() {
this.exchangers.set(
AuthCredentialTypes.OAUTH2,
new OAuth2CredentialExchanger(),
);
this.exchangers.set(
AuthCredentialTypes.OPEN_ID_CONNECT,
new OAuth2CredentialExchanger(),
);
this.exchangers.set(
AuthCredentialTypes.SERVICE_ACCOUNT,
new ServiceAccountCredentialExchanger(),
);
}

@experimental
async exchange(params: {
authScheme?: AuthScheme;
authCredential: AuthCredential;
}): Promise<ExchangeResult> {
const {authCredential, authScheme} = params;

const exchanger = this.exchangers.get(authCredential.authType);

if (!exchanger) {
// If no exchanger found, return the original credential as not exchanged
return {
credential: authCredential,
wasExchanged: false,
};
}

return exchanger.exchange({authScheme, authCredential});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {GoogleAuth, JWT} from 'google-auth-library';
import {
AuthCredential,
AuthCredentialTypes,
ServiceAccount,
} from '../../../../auth/auth_credential.js';
import {AuthScheme} from '../../../../auth/auth_schemes.js';
import {
BaseCredentialExchanger,
CredentialExchangeError,
ExchangeResult,
} from '../../../../auth/exchanger/base_credential_exchanger.js';
import {experimental} from '../../../../utils/experimental.js';

/**
* Fetches credentials for Google Service Account.
* Ported from Python implementation.
*/
@experimental
export class ServiceAccountCredentialExchanger implements BaseCredentialExchanger {
@experimental
async exchange(params: {
authScheme?: AuthScheme;
authCredential: AuthCredential;
}): Promise<ExchangeResult> {
const {authCredential} = params;

if (
authCredential.authType !== AuthCredentialTypes.SERVICE_ACCOUNT ||
!authCredential.serviceAccount
) {
throw new CredentialExchangeError(
'Invalid credential type for ServiceAccountCredentialExchanger',
);
}

const saConfig = authCredential.serviceAccount;

if (saConfig.useDefaultCredential) {
return this.exchangeForDefaultCredential(saConfig);
}

return this.exchangeForExplicitCredential(saConfig);
}

private async exchangeForDefaultCredential(
saConfig: ServiceAccount,
): Promise<ExchangeResult> {
try {
const auth = new GoogleAuth({
scopes: saConfig.scopes || [
'https://www.googleapis.com/auth/cloud-platform',
],
});
const client = await auth.getClient();
const tokenResponse = await client.getAccessToken();
const token = tokenResponse.token;

if (!token) {
throw new Error('Failed to get access token from default credentials');
}

return {
credential: {
authType: AuthCredentialTypes.HTTP,
http: {
scheme: 'bearer',
credentials: {token},
},
},
wasExchanged: true,
};
} catch (error) {
throw new CredentialExchangeError(
`Failed to exchange default service account token: ${(error as Error).message}`,
);
}
}

private async exchangeForExplicitCredential(
saConfig: ServiceAccount,
): Promise<ExchangeResult> {
const creds = saConfig.serviceAccountCredential;
if (!creds) {
throw new CredentialExchangeError(
'Service account credentials are missing.',
);
}

try {
const client = new JWT({
email: creds.clientEmail,
key: creds.privateKey,
scopes: saConfig.scopes,
});

const tokens = await client.authorize();
const token = tokens.access_token;

if (!token) {
throw new Error('Failed to get access token from explicit credentials');
}

return {
credential: {
authType: AuthCredentialTypes.HTTP,
http: {
scheme: 'bearer',
credentials: {token},
},
},
wasExchanged: true,
};
} catch (error) {
throw new CredentialExchangeError(
`Failed to exchange explicit service account token: ${(error as Error).message}`,
);
}
}
}
Loading
Loading