diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index a5dc718f9f77..3211a8f11627 100644 --- a/packages/credential-providers/README.md +++ b/packages/credential-providers/README.md @@ -25,6 +25,7 @@ A collection of all credential providers, with default clients. 1. [SSO login with AWS CLI](#sso-login-with-the-aws-cli) 1. [Sample Files](#sample-files-2) 1. [From Node.js default credentials provider chain](#fromNodeProviderChain) +1. [Creating a custom credentials chain](#chain) ## `fromCognitoIdentity()` @@ -704,14 +705,14 @@ CLI profile name [123456789011_ReadOnly]: my-sso-profile ```javascript //... -const client = new FooClient({ credentials: fromSSO({ profile: "my-sso-profile" })}); +const client = new FooClient({ credentials: fromSSO({ profile: "my-sso-profile" }) }); ``` Alternatively, the SSO credential provider is supported in shared INI credentials provider ```javascript //... -const client = new FooClient({ credentials: fromIni({ profile: "my-sso-profile" })}); +const client = new FooClient({ credentials: fromIni({ profile: "my-sso-profile" }) }); ``` 3. To log out from the current SSO session, use the AWS CLI: @@ -784,6 +785,43 @@ const credentialProvider = fromNodeProviderChain({ }); ``` +## `chain()` + +You can use this helper to create a credential chain of your own. + +```ts +import { fromEnv, fromIni, createCredentialChain } from "@aws-sdk/credential-providers"; +import { S3 } from "@aws-sdk/client-s3"; + +// You can mix existing AWS SDK credential providers +// and custom async functions returning credential objects. +new S3({ + credentials: createCredentialChain( + fromEnv(), + async () => { + // credentials customized by your code... + return credentials; + }, + fromIni() + ), +}); + +// Set a max duration on the credentials (client side only). +// A set expiration will cause the credentials function to be called again +// when the time left is less than 5 minutes. +new S3({ + // expire after 15 minutes (in milliseconds). + credentials: createCredentialChain(fromEnv(), fromIni()).expireAfter(15 * 60_000), +}); + +// Apply shared init properties. +const init = { logger: console }; + +new S3({ + credentials: createCredentialChain(fromEnv(init), fromIni(init)), +}); +``` + ## Add Custom Headers to STS assume-role calls You can specify the plugins--groups of middleware, to inject to the STS client. diff --git a/packages/credential-providers/src/createCredentialChain.spec.ts b/packages/credential-providers/src/createCredentialChain.spec.ts new file mode 100644 index 000000000000..911f41e7b44c --- /dev/null +++ b/packages/credential-providers/src/createCredentialChain.spec.ts @@ -0,0 +1,81 @@ +import { ProviderError } from "@smithy/property-provider"; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; + +import { createCredentialChain } from "./createCredentialChain"; + +describe(createCredentialChain.name, () => { + const mockCredentials: AwsCredentialIdentity = { + accessKeyId: "AKI", + secretAccessKey: "SAK", + }; + + const failure = async () => { + throw new ProviderError("", { tryNextLink: true }); + }; + + it("should throw an error if zero providers are chained", async () => { + const credentialProvider = createCredentialChain(); + + try { + await credentialProvider(); + } catch (e) { + expect(e).toBeDefined(); + } + + expect.assertions(1); + }); + + it("should create a custom chain", async () => { + const credentialProvider = createCredentialChain(async () => mockCredentials); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual(mockCredentials); + }); + + it("should resolve a successful provider function", async () => { + const credentialProvider = createCredentialChain(failure, failure, async () => mockCredentials, failure); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual(mockCredentials); + }); + + it("should resolve the first successful provider function", async () => { + const credentialProvider = createCredentialChain( + failure, + failure, + async () => ({ ...mockCredentials, order: "1st" }), + failure, + async () => ({ ...mockCredentials, order: "2nd" }) + ); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual({ ...mockCredentials, order: "1st" }); + }); + + it("should allow setting a duration", async () => { + const credentialProvider: AwsCredentialIdentityProvider = createCredentialChain( + failure, + failure, + async () => ({ ...mockCredentials, order: "1st" }), + failure, + async () => ({ ...mockCredentials, order: "2nd" }) + ).expireAfter(6 * 60_000); + + const credentials = await credentialProvider(); + + expect(credentials.expiration).toBeDefined(); + expect(credentials.expiration?.getTime()).toBeGreaterThan(Date.now()); + expect(credentials.expiration?.getTime()).toBeLessThan(Date.now() + 375_000); + }); + + it("it should throw an error for durations less than 5 minutes", async () => { + expect(() => { + createCredentialChain(async () => mockCredentials).expireAfter(299_999); + }).toThrow( + "@aws-sdk/credential-providers - createCredentialChain(...).expireAfter(ms) may not be called with a duration lower than five minutes." + ); + }); +}); diff --git a/packages/credential-providers/src/createCredentialChain.ts b/packages/credential-providers/src/createCredentialChain.ts new file mode 100644 index 000000000000..857ce1b5345a --- /dev/null +++ b/packages/credential-providers/src/createCredentialChain.ts @@ -0,0 +1,77 @@ +import { chain as propertyProviderChain } from "@smithy/property-provider"; +import type { AwsCredentialIdentityProvider } from "@smithy/types"; + +export interface CustomCredentialChainOptions { + expireAfter(milliseconds: number): AwsCredentialIdentityProvider & CustomCredentialChainOptions; +} + +/** + * @internal + */ +type Mutable = { + -readonly [Property in keyof Type]: Type[Property]; +}; + +/** + * @example + * ```js + * import { fromEnv, fromIni, createCredentialChain } from '@aws-sdk/credential-providers'; + * import { S3 } from '@aws-sdk/client-s3'; + * + * // You can mix existing AWS SDK credential providers + * // and custom async functions returning credential objects. + * new S3({ + * credentials: createCredentialChain( + * fromEnv(), + * async () => { + * // credentials customized by your code... + * return credentials; + * }, + * fromIni() + * ), + * }); + * + * // Set a max duration on the credentials (client side only). + * // A set expiration will cause the credentials function to be called again + * // when the time left is less than 5 minutes. + * new S3({ + * // expire after 15 minutes (in milliseconds). + * credentials: createCredentialChain(fromEnv(), fromIni()).expireAfter(15 * 60_000), + * }); + * + * // Apply shared init properties. + * const init = { logger: console }; + * + * new S3({ + * credentials: createCredentialChain(fromEnv(init), fromIni(init)), + * }); + * ``` + * + * @param credentialProviders - one or more credential providers. + * @returns a single AwsCredentialIdentityProvider that calls the given + * providers in sequence until one succeeds or all fail. + */ +export const createCredentialChain = ( + ...credentialProviders: AwsCredentialIdentityProvider[] +): AwsCredentialIdentityProvider & CustomCredentialChainOptions => { + let expireAfter = -1; + const baseFunction = async () => { + const credentials = await propertyProviderChain(...credentialProviders)(); + if (!credentials.expiration && expireAfter !== -1) { + (credentials as Mutable).expiration = new Date(Date.now() + expireAfter); + } + return credentials; + }; + const withOptions = Object.assign(baseFunction, { + expireAfter(milliseconds: number) { + if (milliseconds < 5 * 60_000) { + throw new Error( + "@aws-sdk/credential-providers - createCredentialChain(...).expireAfter(ms) may not be called with a duration lower than five minutes." + ); + } + expireAfter = milliseconds; + return withOptions; + }, + }); + return withOptions; +}; diff --git a/packages/credential-providers/src/index.ts b/packages/credential-providers/src/index.ts index a1e419ae6188..e23687adcc6f 100644 --- a/packages/credential-providers/src/index.ts +++ b/packages/credential-providers/src/index.ts @@ -1,8 +1,9 @@ +export * from "./createCredentialChain"; export * from "./fromCognitoIdentity"; export * from "./fromCognitoIdentityPool"; export * from "./fromContainerMetadata"; -export * from "./fromEnv"; export { fromHttp, FromHttpOptions, HttpProviderCredentials } from "@aws-sdk/credential-provider-http"; +export * from "./fromEnv"; export * from "./fromIni"; export * from "./fromInstanceMetadata"; export * from "./fromNodeProviderChain";