Skip to content

Commit

Permalink
feat(credential-providers): add custom credential chain helper (#6374)
Browse files Browse the repository at this point in the history
* feat(credential-providers): add custom credential chain helper

* feat(credential-providers): update example code in readme

Co-authored-by: Trivikram Kamat <[email protected]>

* feat(credential-providers): update docs and minimum duration validation

* feat(credential-providers): rename credential chain file

---------

Co-authored-by: Trivikram Kamat <[email protected]>
  • Loading branch information
kuhe and trivikr authored Aug 12, 2024
1 parent 88d384c commit 1a479dc
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 3 deletions.
42 changes: 40 additions & 2 deletions packages/credential-providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`

Expand Down Expand Up @@ -704,14 +705,14 @@ CLI profile name [123456789011_ReadOnly]: my-sso-profile<ENTER>

```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:
Expand Down Expand Up @@ -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.
Expand Down
81 changes: 81 additions & 0 deletions packages/credential-providers/src/createCredentialChain.spec.ts
Original file line number Diff line number Diff line change
@@ -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."
);
});
});
77 changes: 77 additions & 0 deletions packages/credential-providers/src/createCredentialChain.ts
Original file line number Diff line number Diff line change
@@ -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<Type> = {
-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<typeof credentials>).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;
};
3 changes: 2 additions & 1 deletion packages/credential-providers/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down

0 comments on commit 1a479dc

Please sign in to comment.