Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds support for creating AWS and Identity Pool credentials with custom suppliers. #1783

Merged
merged 3 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 56 additions & 24 deletions src/auth/awsclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import {
} from './baseexternalclient';
import {AuthClientOptions} from './authclient';
import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier';
import {originalOrCamelOptions, SnakeToCamelObject} from '../util';

/**
* AWS credentials JSON interface. This is used for AWS workloads.
*/
export interface AwsClientOptions extends BaseExternalAccountClientOptions {
credential_source: {
credential_source?: {
environment_id: string;
// Region can also be determined from the AWS_REGION or AWS_DEFAULT_REGION
// environment variables.
Expand All @@ -43,6 +44,7 @@ export interface AwsClientOptions extends BaseExternalAccountClientOptions {
// The session token is required for IMDSv2 but optional for IMDSv1
imdsv2_session_token_url?: string;
};
aws_security_credentials_supplier?: AwsSecurityCredentialsSupplier;
}

/**
Expand Down Expand Up @@ -82,12 +84,15 @@ export interface AwsSecurityCredentialsSupplier {
* GCP access token.
*/
export class AwsClient extends BaseExternalAccountClient {
private readonly environmentId: string;
private readonly environmentId?: string;
private readonly awsSecurityCredentialsSupplier: AwsSecurityCredentialsSupplier;
private readonly regionalCredVerificationUrl: string;
private awsRequestSigner: AwsRequestSigner | null;
private region: string;

static #DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL =
'https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15';

/**
* @deprecated AWS client no validates the EC2 metadata address.
**/
Expand All @@ -109,34 +114,61 @@ export class AwsClient extends BaseExternalAccountClient {
* on 401/403 API request errors.
*/
constructor(
options: AwsClientOptions,
options: AwsClientOptions | SnakeToCamelObject<AwsClientOptions>,
additionalOptions?: AuthClientOptions
) {
super(options, additionalOptions);
this.environmentId = options.credential_source.environment_id;
// This is only required if the AWS region is not available in the
// AWS_REGION or AWS_DEFAULT_REGION environment variables.
const regionUrl = options.credential_source.region_url;
// This is only required if AWS security credentials are not available in
// environment variables.
const securityCredentialsUrl = options.credential_source.url;
const imdsV2SessionTokenUrl =
options.credential_source.imdsv2_session_token_url;
this.awsSecurityCredentialsSupplier =
new DefaultAwsSecurityCredentialsSupplier({
regionUrl: regionUrl,
securityCredentialsUrl: securityCredentialsUrl,
imdsV2SessionTokenUrl: imdsV2SessionTokenUrl,
});
const opts = originalOrCamelOptions(options as AwsClientOptions);
const credentialSource = opts.get('credential_source');
const awsSecurityCredentialsSupplier = opts.get(
'aws_security_credentials_supplier'
);
// Validate credential sourcing configuration.
if (!credentialSource && !awsSecurityCredentialsSupplier) {
throw new Error(
'A credential source or AWS security credentials supplier must be specified.'
);
}
if (credentialSource && awsSecurityCredentialsSupplier) {
throw new Error(
'Only one of credential source or AWS security credentials supplier can be specified.'
);
}

if (awsSecurityCredentialsSupplier) {
this.awsSecurityCredentialsSupplier = awsSecurityCredentialsSupplier;
this.regionalCredVerificationUrl =
AwsClient.#DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL;
this.credentialSourceType = 'programmatic';
} else {
const credentialSourceOpts = originalOrCamelOptions(credentialSource);
this.environmentId = credentialSourceOpts.get('environment_id');
// This is only required if the AWS region is not available in the
// AWS_REGION or AWS_DEFAULT_REGION environment variables.
const regionUrl = credentialSourceOpts.get('region_url');
// This is only required if AWS security credentials are not available in
// environment variables.
const securityCredentialsUrl = credentialSourceOpts.get('url');
const imdsV2SessionTokenUrl = credentialSourceOpts.get(
'imdsv2_session_token_url'
);
this.awsSecurityCredentialsSupplier =
new DefaultAwsSecurityCredentialsSupplier({
regionUrl: regionUrl,
securityCredentialsUrl: securityCredentialsUrl,
imdsV2SessionTokenUrl: imdsV2SessionTokenUrl,
});

this.regionalCredVerificationUrl =
options.credential_source.regional_cred_verification_url;
this.regionalCredVerificationUrl = credentialSourceOpts.get(
'regional_cred_verification_url'
);
this.credentialSourceType = 'aws';

// Data validators.
this.validateEnvironmentId();
}
this.awsRequestSigner = null;
this.region = '';
this.credentialSourceType = 'aws';

// Data validators.
this.validateEnvironmentId();
}

private validateEnvironmentId() {
Expand Down
12 changes: 8 additions & 4 deletions src/auth/baseexternalclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const CLOUD_RESOURCE_MANAGER =
/** The workforce audience pattern. */
const WORKFORCE_AUDIENCE_PATTERN =
'//iam\\.googleapis\\.com/locations/[^/]+/workforcePools/[^/]+/providers/.+';
const DEFAULT_TOKEN_URL = 'https://sts.{universeDomain}/v1/token';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../../package.json');
Expand All @@ -75,7 +76,7 @@ export {DEFAULT_UNIVERSE} from './authclient';

export interface SharedExternalAccountClientOptions extends AuthClientOptions {
audience: string;
token_url: string;
token_url?: string;
}

/**
Expand Down Expand Up @@ -108,7 +109,7 @@ export interface ExternalAccountSupplierContext {
*/
export interface BaseExternalAccountClientOptions
extends SharedExternalAccountClientOptions {
type: string;
type?: string;
subject_token_type: string;
service_account_impersonation_url?: string;
service_account_impersonation?: {
Expand Down Expand Up @@ -217,7 +218,8 @@ export abstract class BaseExternalAccountClient extends AuthClient {
options as BaseExternalAccountClientOptions
);

if (opts.get('type') !== EXTERNAL_ACCOUNT_TYPE) {
const type = opts.get('type');
if (type && type !== EXTERNAL_ACCOUNT_TYPE) {
throw new Error(
`Expected "${EXTERNAL_ACCOUNT_TYPE}" type but ` +
`received "${options.type}"`
Expand All @@ -226,7 +228,9 @@ export abstract class BaseExternalAccountClient extends AuthClient {

const clientId = opts.get('client_id');
const clientSecret = opts.get('client_secret');
const tokenUrl = opts.get('token_url');
const tokenUrl =
opts.get('token_url') ??
DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain);
const subjectTokenType = opts.get('subject_token_type');
const workforcePoolUserProject = opts.get('workforce_pool_user_project');
const serviceAccountImpersonationUrl = opts.get(
Expand Down
11 changes: 6 additions & 5 deletions src/auth/externalAccountAuthorizedUserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
*/
export const EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE =
'external_account_authorized_user';
const DEFAULT_TOKEN_URL = 'https://sts.{universeDomain}/v1/oauthtoken';

/**
* External Account Authorized User Credentials JSON interface.
Expand Down Expand Up @@ -171,6 +172,9 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient {
additionalOptions?: AuthClientOptions
) {
super({...options, ...additionalOptions});
if (options.universe_domain) {
this.universeDomain = options.universe_domain;
}
this.refreshToken = options.refresh_token;
const clientAuth = {
confidentialClientType: 'basic',
Expand All @@ -179,7 +183,8 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient {
} as ClientAuthentication;
this.externalAccountAuthorizedUserHandler =
new ExternalAccountAuthorizedUserHandler(
options.token_url,
options.token_url ??
DEFAULT_TOKEN_URL.replace('{universeDomain}', this.universeDomain),
this.transporter,
clientAuth
);
Expand All @@ -197,10 +202,6 @@ export class ExternalAccountAuthorizedUserClient extends AuthClient {
.eagerRefreshThresholdMillis as number;
}
this.forceRefreshOnFailure = !!additionalOptions?.forceRefreshOnFailure;

if (options.universe_domain) {
this.universeDomain = options.universe_domain;
}
}

async getAccessToken(): Promise<{
Expand Down
101 changes: 60 additions & 41 deletions src/auth/identitypoolclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface SubjectTokenSupplier {
*/
export interface IdentityPoolClientOptions
extends BaseExternalAccountClientOptions {
credential_source: {
credential_source?: {
file?: string;
url?: string;
headers?: {
Expand All @@ -64,6 +64,7 @@ export interface IdentityPoolClientOptions
subject_token_field_name?: string;
};
};
subject_token_supplier?: SubjectTokenSupplier;
}

/**
Expand Down Expand Up @@ -97,53 +98,71 @@ export class IdentityPoolClient extends BaseExternalAccountClient {

const opts = originalOrCamelOptions(options as IdentityPoolClientOptions);
const credentialSource = opts.get('credential_source');
const credentialSourceOpts = originalOrCamelOptions(credentialSource);

const formatOpts = originalOrCamelOptions(
credentialSourceOpts.get('format')
);

// Text is the default format type.
const formatType = formatOpts.get('type') || 'text';
const formatSubjectTokenFieldName = formatOpts.get(
'subject_token_field_name'
);

if (formatType !== 'json' && formatType !== 'text') {
throw new Error(`Invalid credential_source format "${formatType}"`);
}
if (formatType === 'json' && !formatSubjectTokenFieldName) {
const subjectTokenSupplier = opts.get('subject_token_supplier');
// Validate credential sourcing configuration.
if (!credentialSource && !subjectTokenSupplier) {
lsirac marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(
'Missing subject_token_field_name for JSON credential_source format'
'A credential source or subject token supplier must be specified.'
);
}

const file = credentialSourceOpts.get('file');
const url = credentialSourceOpts.get('url');
const headers = credentialSourceOpts.get('headers');
if (file && url) {
if (credentialSource && subjectTokenSupplier) {
throw new Error(
'No valid Identity Pool "credential_source" provided, must be either file or url.'
'Only one of credential source or subject token supplier can be specified.'
);
} else if (file && !url) {
this.credentialSourceType = 'file';
this.subjectTokenSupplier = new FileSubjectTokenSupplier({
filePath: file,
formatType: formatType,
subjectTokenFieldName: formatSubjectTokenFieldName,
});
} else if (!file && url) {
this.credentialSourceType = 'url';
this.subjectTokenSupplier = new UrlSubjectTokenSupplier({
url: url,
formatType: formatType,
subjectTokenFieldName: formatSubjectTokenFieldName,
headers: headers,
});
}

if (subjectTokenSupplier) {
this.subjectTokenSupplier = subjectTokenSupplier;
this.credentialSourceType = 'programmatic';
} else {
throw new Error(
'No valid Identity Pool "credential_source" provided, must be either file or url.'
const credentialSourceOpts = originalOrCamelOptions(credentialSource);

const formatOpts = originalOrCamelOptions(
credentialSourceOpts.get('format')
);

// Text is the default format type.
const formatType = formatOpts.get('type') || 'text';
const formatSubjectTokenFieldName = formatOpts.get(
'subject_token_field_name'
);

if (formatType !== 'json' && formatType !== 'text') {
throw new Error(`Invalid credential_source format "${formatType}"`);
}
if (formatType === 'json' && !formatSubjectTokenFieldName) {
throw new Error(
'Missing subject_token_field_name for JSON credential_source format'
);
}

const file = credentialSourceOpts.get('file');
const url = credentialSourceOpts.get('url');
const headers = credentialSourceOpts.get('headers');
if (file && url) {
throw new Error(
'No valid Identity Pool "credential_source" provided, must be either file or url.'
);
} else if (file && !url) {
this.credentialSourceType = 'file';
this.subjectTokenSupplier = new FileSubjectTokenSupplier({
filePath: file,
formatType: formatType,
subjectTokenFieldName: formatSubjectTokenFieldName,
});
} else if (!file && url) {
this.credentialSourceType = 'url';
this.subjectTokenSupplier = new UrlSubjectTokenSupplier({
url: url,
formatType: formatType,
subjectTokenFieldName: formatSubjectTokenFieldName,
headers: headers,
});
} else {
throw new Error(
'No valid Identity Pool "credential_source" provided, must be either file or url.'
);
}
}
}

Expand Down
Loading
Loading