Skip to content

Commit 51aaffc

Browse files
authored
feat(credential-providers): expose node.js default credential provider chain (#3588)
* feat(credential-providers): expose node.js default credential provider chain * feat(credential-providers): address feedbacks * test(credential-providers): refactor unit test import statement * docs(credential-provider-node): mention fromNodeProviderChain in README
1 parent 91f13da commit 51aaffc

14 files changed

+201
-57
lines changed

UPGRADING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ Default credential provider is how SDK resolve the AWS credential if you DO NOT
208208

209209
In Browsers and ReactNative, the chain is empty, meaning you always need supply credentials explicitly.
210210

211-
- **v3**: [defaultProvider](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_provider_node.html#defaultprovider)
211+
- **v3**: [defaultProvider](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_providers#fromnodejsproviderchain-1)
212212
The credential sources and fallback order _does not_ change in v3. It also supports [AWS Single Sign-On credentials](https://aws.amazon.com/single-sign-on/).
213213

214214
### Temporary Credentials

packages/credential-provider-node/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ const provider = defaultProvider({
4545
const client = new S3Client({ credentialDefaultProvider: provider });
4646
```
4747

48+
_IMPORTANT_: We provide a wrapper of this provider in `@aws-sdk/credential-providers`
49+
package to save you from importing `getDefaultRoleAssumerWithWebIdentity()` or
50+
`getDefaultRoleAssume()` from STS package. Similarly, you can do:
51+
52+
```js
53+
const { fromNodeProviderChain } = require("@aws-sdk/credential-providers");
54+
55+
const credentials = fromNodeProviderChain();
56+
57+
const client = new S3Client({ credentials });
58+
```
59+
4860
## Supported configuration
4961

5062
You may customize how credentials are resolved by providing an options hash to

packages/credential-provider-node/src/defaultProvider.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Credentials, MemoizedProvider } from "@aws-sdk/types";
1010

1111
import { remoteProvider } from "./remoteProvider";
1212

13+
export type DefaultProviderInit = FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit;
14+
1315
/**
1416
* Creates a credential provider that will attempt to find credentials from the
1517
* following sources (listed in order of precedence):
@@ -29,24 +31,22 @@ import { remoteProvider } from "./remoteProvider";
2931
* @param init Configuration that is passed to each individual
3032
* provider
3133
*
32-
* @see fromEnv The function used to source credentials from
34+
* @see {@link fromEnv} The function used to source credentials from
3335
* environment variables
34-
* @see fromSSO The function used to source credentials from
36+
* @see {@link fromSSO} The function used to source credentials from
3537
* resolved SSO token cache
36-
* @see fromTokenFile The function used to source credentials from
38+
* @see {@link fromTokenFile} The function used to source credentials from
3739
* token file
38-
* @see fromIni The function used to source credentials from INI
40+
* @see {@link fromIni} The function used to source credentials from INI
3941
* files
40-
* @see fromProcess The function used to sources credentials from
42+
* @see {@link fromProcess} The function used to sources credentials from
4143
* credential_process in INI files
42-
* @see fromInstanceMetadata The function used to source credentials from the
44+
* @see {@link fromInstanceMetadata} The function used to source credentials from the
4345
* EC2 Instance Metadata Service
44-
* @see fromContainerMetadata The function used to source credentials from the
46+
* @see {@link fromContainerMetadata} The function used to source credentials from the
4547
* ECS Container Metadata Service
4648
*/
47-
export const defaultProvider = (
48-
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit = {}
49-
): MemoizedProvider<Credentials> =>
49+
export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider<Credentials> =>
5050
memoize(
5151
chain(
5252
...(init.profile || process.env[ENV_PROFILE] ? [] : [fromEnv()]),

packages/credential-providers/README.md

+41-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ A collection of all credential providers, with default clients.
2323
1. [Supported Configuration](#supported-configuration)
2424
1. [SSO login with AWS CLI](#sso-login-with-the-aws-cli)
2525
1. [Sample Files](#sample-files-2)
26+
1. [From Node.js default credentials provider chain](#fromNodeProviderChain)
2627

2728
## `fromCognitoIdentity()`
2829

@@ -119,7 +120,7 @@ const client = new FooClient({
119120
// Optional. The master credentials used to get and refresh temporary credentials from AWS STS.
120121
// If skipped, it uses the default credential resolved by internal STS client.
121122
masterCredentials: fromTemporaryCredentials({
122-
params: { RoleArn: "arn:aws:iam::1234567890:role/RoleA" }
123+
params: { RoleArn: "arn:aws:iam::1234567890:role/RoleA" },
123124
}),
124125
// Required. Options passed to STS AssumeRole operation.
125126
params: {
@@ -129,16 +130,16 @@ const client = new FooClient({
129130
// session name with prefix of 'aws-sdk-js-'.
130131
RoleSessionName: "aws-sdk-js-123",
131132
// Optional. The duration, in seconds, of the role session.
132-
DurationSeconds: 3600
133+
DurationSeconds: 3600,
133134
// ... For more options see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
134135
},
135136
// Optional. Custom STS client configurations overriding the default ones.
136137
clientConfig: { region },
137138
// Optional. A function that returns a promise fulfilled with an MFA token code for the provided
138139
// MFA Serial code. Required if `params` has `SerialNumber` config.
139-
mfaCodeProvider: async mfaSerial => {
140-
return "token"
141-
}
140+
mfaCodeProvider: async (mfaSerial) => {
141+
return "token";
142+
},
142143
}),
143144
});
144145
```
@@ -593,7 +594,7 @@ Successfully signed out of all SSO profiles.
593594
### Sample files
594595
595596
This credential provider is only applicable if the profile specified in shared configuration and
596-
credentials files contain ALL of the following entries:
597+
credentials files contain ALL of the following entries.
597598
598599
#### `~/.aws/credentials`
599600
@@ -615,6 +616,40 @@ sso_role_name = SampleRole
615616
sso_start_url = https://d-abc123.awsapps.com/start
616617
```
617618
619+
## `fromNodeProviderChain()`
620+
621+
The credential provider used as default in the Node.js clients, but with default role assumers so
622+
you don't need to import them from STS client and supply them manually. You normally don't need
623+
to use this explicitly in the client constructor. It is useful for utility functions requiring
624+
credentials like S3 presigner, or RDS signer.
625+
626+
This credential provider will attempt to find credentials from the following sources (listed in
627+
order of precedence):
628+
629+
- [Environment variables exposed via `process.env`](#fromenv)
630+
- [SSO credentials from token cache](#fromsso)
631+
- [Web identity token credentials](#fromtokenfile)
632+
- [Shared credentials and config ini files](#fromini)
633+
- [The EC2/ECS Instance Metadata Service](#fromcontainermetadata-and-frominstancemetadata)
634+
635+
This credential provider will invoke one provider at a time and only
636+
continue to the next if no credentials have been located. For example, if
637+
the process finds values defined via the `AWS_ACCESS_KEY_ID` and
638+
`AWS_SECRET_ACCESS_KEY` environment variables, the files at
639+
`~/.aws/credentials` and `~/.aws/config` will not be read, nor will any
640+
messages be sent to the Instance Metadata Service
641+
642+
```js
643+
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; // ES6 import
644+
// const { fromNodeProviderChain } = require("@aws-sdk/credential-providers") // CommonJS import
645+
const credentialProvider = fromNodeProviderChain({
646+
//...any input of fromEnv(), fromSSO(), fromTokenFile(), fromIni(),
647+
// fromProcess(), fromInstanceMetadata(), fromContainerMetadata()
648+
// Optional. Custom STS client configurations overriding the default ones.
649+
clientConfig: { region },
650+
});
651+
```
652+
618653
[getcredentialsforidentity_api]: https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html
619654
[getid_api]: https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetId.html
620655
[assumerole_api]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html

packages/credential-providers/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@aws-sdk/credential-provider-env": "*",
3434
"@aws-sdk/credential-provider-imds": "*",
3535
"@aws-sdk/credential-provider-ini": "*",
36+
"@aws-sdk/credential-provider-node": "*",
3637
"@aws-sdk/credential-provider-process": "*",
3738
"@aws-sdk/credential-provider-sso": "*",
3839
"@aws-sdk/credential-provider-web-identity": "*",

packages/credential-providers/src/fromCognitoIdentity.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
2+
import { fromCognitoIdentity as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";
3+
4+
import { fromCognitoIdentity } from "./fromCognitoIdentity";
5+
16
jest.mock("@aws-sdk/client-cognito-identity", () => ({
27
CognitoIdentityClient: jest.fn().mockImplementation(function () {
38
return "COGNITO_IDENTITY_CLIENT";
49
}),
510
}));
611

7-
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
8-
import { fromCognitoIdentity as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";
9-
10-
import { fromCognitoIdentity } from "./fromCognitoIdentity";
11-
1212
jest.mock("@aws-sdk/credential-provider-cognito-identity", () => ({
1313
fromCognitoIdentity: jest.fn(),
1414
}));

packages/credential-providers/src/fromCognitoIdentityPool.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
2+
import { fromCognitoIdentityPool as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";
3+
4+
import { fromCognitoIdentityPool } from "./fromCognitoIdentityPool";
5+
16
jest.mock("@aws-sdk/client-cognito-identity", () => ({
27
CognitoIdentityClient: jest.fn().mockImplementation(function () {
38
return "COGNITO_IDENTITY_CLIENT";
49
}),
510
}));
611

7-
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
8-
import { fromCognitoIdentityPool as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";
9-
10-
import { fromCognitoIdentityPool } from "./fromCognitoIdentityPool";
11-
1212
jest.mock("@aws-sdk/credential-provider-cognito-identity", () => ({
1313
fromCognitoIdentityPool: jest.fn(),
1414
}));

packages/credential-providers/src/fromIni.spec.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
const ROLE_ASSUMER = "ROLE_ASSUMER";
2-
const ROLE_ASSUMER_WITH_WEB_IDENTITY = "ROLE_ASSUMER_WITH_WEB_IDENTITY";
3-
4-
jest.mock("@aws-sdk/client-sts", () => ({
5-
getDefaultRoleAssumer: jest.fn().mockReturnValue(ROLE_ASSUMER),
6-
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockReturnValue(ROLE_ASSUMER_WITH_WEB_IDENTITY),
7-
}));
8-
91
import { getDefaultRoleAssumer, getDefaultRoleAssumerWithWebIdentity } from "@aws-sdk/client-sts";
102
import { fromIni as coreProvider } from "@aws-sdk/credential-provider-ini";
113

124
import { fromIni } from "./fromIni";
135

6+
const mockRoleAssumer = jest.fn().mockResolvedValue("ROLE_ASSUMER");
7+
const mockRoleAssumerWithWebIdentity = jest.fn().mockResolvedValue("ROLE_ASSUMER_WITH_WEB_IDENTITY");
8+
9+
jest.mock("@aws-sdk/client-sts", () => ({
10+
getDefaultRoleAssumer: jest.fn().mockImplementation(() => mockRoleAssumer),
11+
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockImplementation(() => mockRoleAssumerWithWebIdentity),
12+
}));
13+
1414
jest.mock("@aws-sdk/credential-provider-ini", () => ({
1515
fromIni: jest.fn(),
1616
}));
@@ -25,8 +25,8 @@ describe("fromIni", () => {
2525
fromIni({ profile });
2626
expect(coreProvider).toBeCalledWith({
2727
profile,
28-
roleAssumer: ROLE_ASSUMER,
29-
roleAssumerWithWebIdentity: ROLE_ASSUMER_WITH_WEB_IDENTITY,
28+
roleAssumer: mockRoleAssumer,
29+
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
3030
});
3131
expect(getDefaultRoleAssumer).toBeCalled();
3232
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalled();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { getDefaultRoleAssumer, getDefaultRoleAssumerWithWebIdentity } from "@aws-sdk/client-sts";
2+
import { defaultProvider } from "@aws-sdk/credential-provider-node";
3+
4+
import { fromNodeProviderChain } from "./fromNodeProviderChain";
5+
6+
const mockRoleAssumer = jest.fn().mockResolvedValue("ROLE_ASSUMER");
7+
const mockRoleAssumerWithWebIdentity = jest.fn().mockResolvedValue("ROLE_ASSUMER_WITH_WEB_IDENTITY");
8+
9+
jest.mock("@aws-sdk/client-sts", () => ({
10+
getDefaultRoleAssumer: jest.fn().mockImplementation(() => mockRoleAssumer),
11+
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockImplementation(() => mockRoleAssumerWithWebIdentity),
12+
}));
13+
14+
jest.mock("@aws-sdk/credential-provider-node", () => ({
15+
defaultProvider: jest.fn(),
16+
}));
17+
18+
describe(fromNodeProviderChain.name, () => {
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
it("should inject default role assumers", () => {
24+
const profile = "profile";
25+
fromNodeProviderChain({ profile });
26+
expect(defaultProvider).toBeCalledWith({
27+
profile,
28+
roleAssumer: mockRoleAssumer,
29+
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
30+
});
31+
expect(getDefaultRoleAssumer).toBeCalled();
32+
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalled();
33+
});
34+
35+
it("should use supplied role assumers", () => {
36+
const profile = "profile";
37+
const roleAssumer = jest.fn();
38+
const roleAssumerWithWebIdentity = jest.fn();
39+
fromNodeProviderChain({ profile, roleAssumer, roleAssumerWithWebIdentity });
40+
expect(defaultProvider).toBeCalledWith({
41+
profile,
42+
roleAssumer,
43+
roleAssumerWithWebIdentity,
44+
});
45+
expect(getDefaultRoleAssumer).not.toBeCalled();
46+
expect(getDefaultRoleAssumerWithWebIdentity).not.toBeCalled();
47+
});
48+
49+
it("should use supplied sts options", () => {
50+
const profile = "profile";
51+
const clientConfig = {
52+
region: "US_BAR_1",
53+
};
54+
fromNodeProviderChain({ profile, clientConfig });
55+
expect(getDefaultRoleAssumer).toBeCalledWith(clientConfig);
56+
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalledWith(clientConfig);
57+
});
58+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { getDefaultRoleAssumer, getDefaultRoleAssumerWithWebIdentity, STSClientConfig } from "@aws-sdk/client-sts";
2+
import { defaultProvider, DefaultProviderInit } from "@aws-sdk/credential-provider-node";
3+
import { CredentialProvider } from "@aws-sdk/types";
4+
5+
export interface fromNodeProviderChainInit extends DefaultProviderInit {
6+
clientConfig?: STSClientConfig;
7+
}
8+
9+
/**
10+
* This is the same credential provider as {@link defaultProvider|the default provider for Node.js SDK},
11+
* but with default role assumers so you don't need to import them from
12+
* STS client and supply them manually.
13+
*
14+
* You normally don't need to use this explicitly in the client constructor.
15+
* It is useful for utility functions requiring credentials like S3 presigner,
16+
* or RDS signer.
17+
*
18+
* ```js
19+
* import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; // ES6 import
20+
* // const { fromNodeProviderChain } = require("@aws-sdk/credential-providers") // CommonJS import
21+
*
22+
* const credentialProvider = fromNodeProviderChain({
23+
* //...any input of fromEnv(), fromSSO(), fromTokenFile(), fromIni(),
24+
* // fromProcess(), fromInstanceMetadata(), fromContainerMetadata()
25+
*
26+
* // Optional. Custom STS client configurations overriding the default ones.
27+
* clientConfig: { region },
28+
* })
29+
* ```
30+
*/
31+
export const fromNodeProviderChain = (init: fromNodeProviderChainInit = {}): CredentialProvider =>
32+
defaultProvider({
33+
...init,
34+
roleAssumer: init.roleAssumer ?? getDefaultRoleAssumer(init.clientConfig),
35+
roleAssumerWithWebIdentity:
36+
init.roleAssumerWithWebIdentity ?? getDefaultRoleAssumerWithWebIdentity(init.clientConfig),
37+
});

packages/credential-providers/src/fromTemporaryCredentials.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
2+
3+
import { fromTemporaryCredentials } from "./fromTemporaryCredentials";
4+
15
const sendMock = jest.fn();
26
jest.mock("@aws-sdk/client-sts", () => ({
37
STSClient: jest.fn().mockImplementation((config) => ({
@@ -17,10 +21,6 @@ jest.mock("@aws-sdk/client-sts", () => ({
1721
}),
1822
}));
1923

20-
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
21-
22-
import { fromTemporaryCredentials } from "./fromTemporaryCredentials";
23-
2424
describe("fromTemporaryCredentials", () => {
2525
const RoleArn = "ROLE_ARN";
2626
const RoleSessionName = "ROLE_SESSION_NAME";

packages/credential-providers/src/fromTokenFile.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
const ROLE_ASSUMER_WITH_WEB_IDENTITY = "ROLE_ASSUMER_WITH_WEB_IDENTITY";
2-
3-
jest.mock("@aws-sdk/client-sts", () => ({
4-
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockReturnValue(ROLE_ASSUMER_WITH_WEB_IDENTITY),
5-
}));
6-
71
import { getDefaultRoleAssumerWithWebIdentity } from "@aws-sdk/client-sts";
82
import { fromTokenFile as coreProvider } from "@aws-sdk/credential-provider-web-identity";
93

104
import { fromTokenFile } from "./fromTokenFile";
115

6+
const mockRoleAssumerWithWebIdentity = jest.fn().mockResolvedValue("ROLE_ASSUMER_WITH_WEB_IDENTITY");
7+
8+
jest.mock("@aws-sdk/client-sts", () => ({
9+
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockImplementation(() => mockRoleAssumerWithWebIdentity),
10+
}));
11+
1212
jest.mock("@aws-sdk/credential-provider-web-identity", () => ({
1313
fromTokenFile: jest.fn(),
1414
}));
@@ -21,7 +21,7 @@ describe("fromTokenFile", () => {
2121
it("should inject default role assumer", () => {
2222
fromTokenFile();
2323
expect(coreProvider).toBeCalledWith({
24-
roleAssumerWithWebIdentity: ROLE_ASSUMER_WITH_WEB_IDENTITY,
24+
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
2525
});
2626
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalled();
2727
});
@@ -34,7 +34,7 @@ describe("fromTokenFile", () => {
3434
clientConfig,
3535
});
3636
expect((coreProvider as jest.Mock).mock.calls[0][0]).toMatchObject({
37-
roleAssumerWithWebIdentity: ROLE_ASSUMER_WITH_WEB_IDENTITY,
37+
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
3838
});
3939
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalledWith(clientConfig);
4040
});

0 commit comments

Comments
 (0)