diff --git a/packages/credential-providers/README.md b/packages/credential-providers/README.md index a5dc718f9f776..1cebec7f81370 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,37 @@ const credentialProvider = fromNodeProviderChain({ }); ``` +## `chain()` + +You can use this helper to create a credential chain of your own. + +```ts +import { fromEnv, fromIni, chain } 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: chain(fromEnv(), fromIni(), async () => { + return { + ...myCredentialsFromSomewhereElse, + }; + }), +}); + +// Set a max duration on the credentials (client side only). +// A set expiration will cause the credentials function to be called again +// after the given duration. +new S3({ + credentials: chain(fromEnv(), fromIni()).expireAfter(15 * 60_000), // 15 minutes in milliseconds. +}); + +// apply shared init properties. +new S3({ + credentials: chain(...[fromEnv, fromIni].map((p) => p({ logger: console }))), +}); +``` + ## 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/customCredentialChain.spec.ts b/packages/credential-providers/src/customCredentialChain.spec.ts new file mode 100644 index 0000000000000..9a45e3c629efe --- /dev/null +++ b/packages/credential-providers/src/customCredentialChain.spec.ts @@ -0,0 +1,73 @@ +import { ProviderError } from "@smithy/property-provider"; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types"; + +import { chain } from "./customCredentialChain"; + +describe(chain.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 = chain(); + + try { + await credentialProvider(); + } catch (e) { + expect(e).toBeDefined(); + } + + expect.assertions(1); + }); + + it("should create a custom chain", async () => { + const credentialProvider = chain(async () => mockCredentials); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual(mockCredentials); + }); + + it("should resolve a successful provider function", async () => { + const credentialProvider = chain(failure, failure, async () => mockCredentials, failure); + + const credentials = await credentialProvider(); + + expect(credentials).toEqual(mockCredentials); + }); + + it("should resolve the first successful provider function", async () => { + const credentialProvider = chain( + 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 = chain( + failure, + failure, + async () => ({ ...mockCredentials, order: "1st" }), + failure, + async () => ({ ...mockCredentials, order: "2nd" }) + ).expireAfter(15_000); + + const credentials = await credentialProvider(); + + expect(credentials.expiration).toBeDefined(); + expect(credentials.expiration?.getTime()).toBeGreaterThan(Date.now()); + expect(credentials.expiration?.getTime()).toBeLessThan(Date.now() + 30_000); + }); +}); diff --git a/packages/credential-providers/src/customCredentialChain.ts b/packages/credential-providers/src/customCredentialChain.ts new file mode 100644 index 0000000000000..32915351080aa --- /dev/null +++ b/packages/credential-providers/src/customCredentialChain.ts @@ -0,0 +1,69 @@ +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, chain } from "@aws-sdk/credential-providers"; + * import { S3 } from '@aws-sdk/client-s3'; + * + * // basic chain. + * new S3({ + * credentials: chain( + * fromEnv(), + * fromIni() + * ) + * }); + * + * // set a max duration on the credentials (client side only). + * new S3({ + * credentials: chain( + * fromEnv(), + * fromIni() + * ).expireAfter(15 * 60_000) // 15 minutes in milliseconds. + * }); + * + * // apply shared init properties. + * new S3({ + * credentials: chain(...[ + * fromEnv, + * fromIni + * ].map(p => p({ logger: console }))) + * }); + * + * ``` + * + * @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 chain = ( + ...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) { + expireAfter = milliseconds; + return withOptions; + }, + }); + return withOptions; +}; diff --git a/packages/credential-providers/src/index.ts b/packages/credential-providers/src/index.ts index a1e419ae6188c..1845e3413d68d 100644 --- a/packages/credential-providers/src/index.ts +++ b/packages/credential-providers/src/index.ts @@ -1,8 +1,9 @@ +export * from "./customCredentialChain"; 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";