Skip to content

Commit

Permalink
feat(credential-providers): add custom credential chain helper
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed Aug 9, 2024
1 parent 628ffe5 commit c4f2c39
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 3 deletions.
2 changes: 2 additions & 0 deletions clients/client-s3/src/commands/GetObjectCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,5 @@ export class GetObjectCommand extends $Command
.ser(se_GetObjectCommand)
.de(de_GetObjectCommand)
.build() {}

GetObjectCommand.getEndpointParameterInstructions;
40 changes: 38 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,41 @@ 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).
new S3({
credentials: chain(fromEnv(), fromIni()).expireAfter(15 * 60_000), // 15 minutes in milliseconds.
});

// A set expiration will cause the credentials function to be called
// when the Date nears.
new S3({
credentials: chain(fromEnv(), fromIni()).withExpiration(new Date(Date.now() + 15 * 60_000)),
});

// 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.
Expand Down
88 changes: 88 additions & 0 deletions packages/credential-providers/src/customCredentialChain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 an expiration", async () => {
const credentialProvider: AwsCredentialIdentityProvider = chain(
failure,
failure,
async () => ({ ...mockCredentials, order: "1st" }),
failure,
async () => ({ ...mockCredentials, order: "2nd" })
).withExpiration(new Date("2024-08-09T19:53:27.900Z"));

const credentials = await credentialProvider();

expect(credentials.expiration).toBeDefined();
expect(credentials.expiration?.getTime()).toEqual(1723233207900);
});

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);
});
});
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 {
withExpiration(expiration: Date): AwsCredentialIdentityProvider & CustomCredentialChainOptions;
expireAfter(milliseconds: number): AwsCredentialIdentityProvider & CustomCredentialChainOptions;
}

/**
* @internal
*/
type Mutable<Type> = {
-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 expiration: Date | undefined = undefined;
let expireAfter = -1;
const baseFunction = async () => {
const credentials = await propertyProviderChain(...credentialProviders)();
if ((expiration?.getTime?.() ?? Infinity) < (credentials.expiration?.getTime?.() ?? Infinity)) {
(credentials as Mutable<typeof credentials>).expiration = expiration;
} else if (!credentials.expiration && expireAfter !== -1) {
(credentials as Mutable<typeof credentials>).expiration = new Date(Date.now() + expireAfter);
}
return credentials;
};
const withOptions = Object.assign(baseFunction, {
withExpiration(_expiration: Date) {
expiration = _expiration;
return withOptions;
},
expireAfter(milliseconds: number) {
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

0 comments on commit c4f2c39

Please sign in to comment.