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(credential-providers): add custom credential chain helper #6374

Merged
merged 4 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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/customCredentialChain.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 "./customCredentialChain";

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/customCredentialChain.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];
trivikr marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* @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)),
* });
kuhe marked this conversation as resolved.
Show resolved Hide resolved
* ```
*
* @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 = (
kuhe marked this conversation as resolved.
Show resolved Hide resolved
...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 "./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";
Expand Down
Loading