Skip to content

Commit 3dd31c4

Browse files
Eric HayesAllanZhengYP
Eric Hayes
andauthored
feat(credential-provider-node): add web identity provider to credential provider chain (#2260)
* feat(credential-provider-node): add support for web identity provider Co-authored-by: AllanZhengYP <[email protected]>
1 parent 2384e24 commit 3dd31c4

File tree

7 files changed

+154
-12
lines changed

7 files changed

+154
-12
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@
118118
],
119119
"**/*.{ts,js,md,json}": "prettier --write"
120120
}
121-
}
121+
}

packages/credential-provider-node/README.md

+33-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
This module provides a factory function, `fromEnv`, that will attempt to source
99
AWS credentials from a Node.JS environment. It will attempt to find credentials
1010
from the following sources (listed in order of precedence):
11-
_ Environment variables exposed via `process.env`
12-
_ Shared credentials and config ini files \* The EC2/ECS Instance Metadata Service
11+
12+
- Environment variables exposed via `process.env`
13+
- SSO credentials from token cache
14+
- Web identity token credentials
15+
- Shared credentials and config ini files
16+
- The EC2/ECS Instance Metadata Service
1317

1418
The default credential provider will invoke one provider at a time and only
1519
continue to the next if no credentials have been located. For example, if the
@@ -23,6 +27,24 @@ If invalid configuration is encountered (such as a profile in
2327
that does not exist), then the chained provider will be rejected with an error
2428
and will not invoke the next provider in the list.
2529

30+
_IMPORTANT_: if you intend for your code to run using EKS roles at some point
31+
(for example in a production environment, but not when working locally) then
32+
you must explicitly specify a value for `roleAssumerWithWebIdentity`. There is a
33+
default function available in `@aws-sdk/client-sts` package. An example of using
34+
this:
35+
36+
```js
37+
const { getDefaultRoleAssumerWithWebIdentity } = require("@aws-sdk/client-sts");
38+
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
39+
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
40+
41+
const provider = defaultProvider({
42+
roleAssumerWithWebIdentity: getDefaultRoleAssumerWithWebIdentity,
43+
});
44+
45+
const client = new S3Client({ credentialDefaultProvider: provider });
46+
```
47+
2648
## Supported configuration
2749

2850
You may customize how credentials are resolved by providing an options hash to
@@ -45,6 +67,13 @@ supported:
4567
- `roleAssumer` - A function that assumes a role and returns a promise
4668
fulfilled with credentials for the assumed role. If not specified, the SDK
4769
will create an STS client and call its `assumeRole` method.
70+
- `roleArn` - ARN to assume. If not specified, the provider will use the value
71+
in the `AWS_ROLE_ARN` environment variable.
72+
- `webIdentityTokenFile` - File location of where the `OIDC` token is stored.
73+
If not specified, the provider will use the value in the `AWS_WEB_IDENTITY_TOKEN_FILE`
74+
environment variable.
75+
- `roleAssumerWithWebIdentity` - A function that assumes a role with web identity and
76+
returns a promise fulfilled with credentials for the assumed role.
4877
- `timeout` - The connection timeout (in milliseconds) to apply to any remote
4978
requests. If not specified, a default value of `1000` (one second) is used.
5079
- `maxRetries` - The maximum number of times any HTTP connections should be
@@ -53,6 +82,8 @@ supported:
5382
## Related packages:
5483

5584
- [AWS Credential Provider for Node.JS - Environment Variables](../credential-provider-env)
85+
- [AWS Credential Provider for Node.JS - SSO](../credential-provider-sso)
86+
- [AWS Credential Provider for Node.JS - Web Identity](../credential-provider-web-identity)
5687
- [AWS Credential Provider for Node.JS - Shared Configuration Files](../credential-provider-ini)
5788
- [AWS Credential Provider for Node.JS - Instance and Container Metadata](../credential-provider-imds)
5889
- [AWS Shared Configuration File Loader](../shared-ini-file-loader)

packages/credential-provider-node/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"license": "Apache-2.0",
2626
"dependencies": {
2727
"@aws-sdk/credential-provider-env": "3.13.1",
28+
"@aws-sdk/credential-provider-web-identity": "3.13.1",
2829
"@aws-sdk/credential-provider-imds": "3.13.1",
2930
"@aws-sdk/credential-provider-ini": "3.13.1",
3031
"@aws-sdk/credential-provider-process": "3.13.1",

packages/credential-provider-node/src/index.spec.ts

+70
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ jest.mock("@aws-sdk/credential-provider-sso", () => {
1212
});
1313
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
1414

15+
jest.mock("@aws-sdk/credential-provider-web-identity", () => {
16+
const webIdentityProvider = jest.fn();
17+
return {
18+
fromTokenFile: jest.fn().mockReturnValue(webIdentityProvider),
19+
};
20+
});
21+
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
22+
1523
jest.mock("@aws-sdk/credential-provider-ini", () => {
1624
const iniProvider = jest.fn();
1725
return {
@@ -129,6 +137,29 @@ describe("defaultProvider", () => {
129137
expect((fromSSO() as any).mock.calls.length).toBe(1);
130138
expect((fromIni() as any).mock.calls.length).toBe(0);
131139
expect((fromProcess() as any).mock.calls.length).toBe(0);
140+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
141+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
142+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
143+
});
144+
145+
it("should stop after the Web Identity provider if credentials have been found", async () => {
146+
const creds = {
147+
accessKeyId: "foo",
148+
secretAccessKey: "bar",
149+
};
150+
151+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
152+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
153+
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
154+
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
155+
(fromTokenFile() as any).mockImplementation(() => Promise.resolve(creds));
156+
157+
expect(await defaultProvider()()).toEqual(creds);
158+
expect((fromEnv() as any).mock.calls.length).toBe(1);
159+
expect((fromSSO() as any).mock.calls.length).toBe(1);
160+
expect((fromIni() as any).mock.calls.length).toBe(1);
161+
expect((fromProcess() as any).mock.calls.length).toBe(1);
162+
expect((fromTokenFile() as any).mock.calls.length).toBe(1);
132163
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
133164
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
134165
});
@@ -141,12 +172,14 @@ describe("defaultProvider", () => {
141172

142173
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
143174
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
175+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
144176
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
145177

146178
expect(await defaultProvider()()).toEqual(creds);
147179
expect((fromEnv() as any).mock.calls.length).toBe(1);
148180
expect((fromSSO() as any).mock.calls.length).toBe(1);
149181
expect((fromIni() as any).mock.calls.length).toBe(1);
182+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
150183
expect((fromProcess() as any).mock.calls.length).toBe(0);
151184
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
152185
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
@@ -168,6 +201,7 @@ describe("defaultProvider", () => {
168201
expect((fromSSO() as any).mock.calls.length).toBe(1);
169202
expect((fromIni() as any).mock.calls.length).toBe(1);
170203
expect((fromProcess() as any).mock.calls.length).toBe(1);
204+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
171205
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
172206
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
173207
});
@@ -180,13 +214,15 @@ describe("defaultProvider", () => {
180214
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
181215
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
182216
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
217+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
183218
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
184219
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
185220

186221
expect(await defaultProvider()()).toEqual(creds);
187222
expect((fromEnv() as any).mock.calls.length).toBe(1);
188223
expect((fromSSO() as any).mock.calls.length).toBe(1);
189224
expect((fromIni() as any).mock.calls.length).toBe(1);
225+
expect((fromTokenFile() as any).mock.calls.length).toBe(1);
190226
expect((fromProcess() as any).mock.calls.length).toBe(1);
191227
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
192228
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(1);
@@ -201,6 +237,7 @@ describe("defaultProvider", () => {
201237
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
202238
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
203239
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
240+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
204241
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
205242
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
206243

@@ -220,6 +257,7 @@ describe("defaultProvider", () => {
220257
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
221258
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
222259
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
260+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
223261
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
224262
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
225263
(fromContainerMetadata() as any).mockImplementation(() => Promise.resolve(creds));
@@ -230,6 +268,7 @@ describe("defaultProvider", () => {
230268
expect((fromEnv() as any).mock.calls.length).toBe(1);
231269
expect((fromSSO() as any).mock.calls.length).toBe(1);
232270
expect((fromIni() as any).mock.calls.length).toBe(1);
271+
expect((fromTokenFile() as any).mock.calls.length).toBe(1);
233272
expect((fromProcess() as any).mock.calls.length).toBe(1);
234273
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
235274
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
@@ -244,13 +283,15 @@ describe("defaultProvider", () => {
244283
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
245284
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
246285
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
286+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
247287
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
248288
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
249289

250290
await expect(defaultProvider()()).resolves;
251291
expect((loadSharedConfigFiles as any).mock.calls.length).toBe(1);
252292
expect((fromIni as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
253293
expect((fromSSO as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
294+
expect((fromTokenFile as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
254295
expect((fromProcess as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
255296
});
256297

@@ -277,6 +318,29 @@ describe("defaultProvider", () => {
277318
expect((fromSSO as any).mock.calls[0][0]).toEqual({ ...ssoConfig, loadedConfig });
278319
});
279320

321+
it("should pass configuration on to the Web Identity provider", async () => {
322+
const webIdentityConfig: FromTokenFileInit = {
323+
roleArn: "someRoleArn",
324+
webIdentityTokenFile: "/home/user/.secrets/tokenFile",
325+
};
326+
327+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
328+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
329+
(fromTokenFile() as any).mockImplementation(() =>
330+
Promise.resolve({
331+
accessKeyId: "foo",
332+
secretAccessKey: "bar",
333+
})
334+
);
335+
336+
(fromTokenFile as any).mockClear();
337+
338+
await expect(defaultProvider(webIdentityConfig)()).resolves;
339+
340+
expect((fromTokenFile as any).mock.calls.length).toBe(1);
341+
expect((fromTokenFile as any).mock.calls[0][0]).toEqual({ ...webIdentityConfig, loadedConfig });
342+
});
343+
280344
it("should pass configuration on to the ini provider", async () => {
281345
const iniConfig: FromIniInit = {
282346
profile: "foo",
@@ -443,13 +507,15 @@ describe("defaultProvider", () => {
443507
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
444508
(fromSSO() as any).mockImplementation(() => Promise.resolve(Promise.resolve(creds)));
445509
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
510+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
446511
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
447512
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
448513

449514
expect(await defaultProvider({ profile: "foo" })()).toEqual(creds);
450515
expect((fromEnv() as any).mock.calls.length).toBe(0);
451516
expect((fromSSO() as any).mock.calls.length).toBe(1);
452517
expect((fromIni() as any).mock.calls.length).toBe(0);
518+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
453519
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
454520
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
455521
});
@@ -463,6 +529,7 @@ describe("defaultProvider", () => {
463529
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
464530
(fromSSO() as any).mockImplementation(() => Promise.resolve(creds));
465531
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
532+
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
466533
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
467534
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
468535
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
@@ -472,6 +539,7 @@ describe("defaultProvider", () => {
472539
expect((fromEnv() as any).mock.calls.length).toBe(0);
473540
expect((fromSSO() as any).mock.calls.length).toBe(1);
474541
expect((fromIni() as any).mock.calls.length).toBe(0);
542+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
475543
expect((fromProcess() as any).mock.calls.length).toBe(0);
476544
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
477545
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
@@ -493,6 +561,7 @@ describe("defaultProvider", () => {
493561
expect((fromEnv() as any).mock.calls.length).toBe(0);
494562
expect((fromSSO() as any).mock.calls.length).toBe(1);
495563
expect((fromIni() as any).mock.calls.length).toBe(1);
564+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
496565
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
497566
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
498567
});
@@ -516,6 +585,7 @@ describe("defaultProvider", () => {
516585
expect((fromSSO() as any).mock.calls.length).toBe(1);
517586
expect((fromIni() as any).mock.calls.length).toBe(1);
518587
expect((fromProcess() as any).mock.calls.length).toBe(1);
588+
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
519589
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
520590
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
521591
});

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { ENV_PROFILE, fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
1010
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process";
1111
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
12+
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
1213
import { chain, memoize, ProviderError } from "@aws-sdk/property-provider";
1314
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
1415
import { CredentialProvider } from "@aws-sdk/types";
@@ -19,6 +20,8 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
1920
* Creates a credential provider that will attempt to find credentials from the
2021
* following sources (listed in order of precedence):
2122
* * Environment variables exposed via `process.env`
23+
* * SSO credentials from token cache
24+
* * Web identity token credentials
2225
* * Shared credentials and config ini files
2326
* * The EC2/ECS Instance Metadata Service
2427
*
@@ -36,6 +39,8 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
3639
* environment variables
3740
* @see fromSSO The function used to source credentials from
3841
* resolved SSO token cache
42+
* @see fromTokenFile The function used to source credentials from
43+
* token file
3944
* @see fromIni The function used to source credentials from INI
4045
* files
4146
* @see fromProcess The function used to sources credentials from
@@ -46,11 +51,11 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
4651
* ECS Container Metadata Service
4752
*/
4853
export const defaultProvider = (
49-
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit = {}
54+
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit = {}
5055
): CredentialProvider => {
5156
const options = { profile: process.env[ENV_PROFILE], ...init };
5257
if (!options.loadedConfig) options.loadedConfig = loadSharedConfigFiles(init);
53-
const providers = [fromSSO(options), fromIni(options), fromProcess(options), remoteProvider(options)];
58+
const providers = [fromSSO(options), fromIni(options), fromProcess(options), fromTokenFile(options), remoteProvider(options)];
5459
if (!options.profile) providers.unshift(fromEnv());
5560
const providerChain = chain(...providers);
5661

packages/credential-provider-web-identity/src/fromTokenFile.spec.ts

+24
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const ENV_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
99
const ENV_ROLE_ARN = "AWS_ROLE_ARN";
1010
const ENV_ROLE_SESSION_NAME = "AWS_ROLE_SESSION_NAME";
1111

12+
import { ProviderError } from "@aws-sdk/property-provider";
13+
1214
jest.mock("fs");
1315

1416
const MOCK_CREDS = {
@@ -114,5 +116,27 @@ describe(fromTokenFile.name, () => {
114116
}
115117
expect(readFileSync).toHaveBeenCalledTimes(1);
116118
});
119+
120+
it("throws if web_identity_token_file is not specified", async () => {
121+
try {
122+
delete process.env[ENV_TOKEN_FILE];
123+
await fromTokenFile()();
124+
fail(`Expected error to be thrown`);
125+
} catch (error) {
126+
expect(error).toBeInstanceOf(ProviderError);
127+
expect(error.tryNextLink).toBe(true);
128+
}
129+
});
130+
131+
it("throws if role_arn is not specified", async () => {
132+
try {
133+
delete process.env[ENV_ROLE_ARN];
134+
await fromTokenFile()();
135+
fail(`Expected error to be thrown`);
136+
} catch (error) {
137+
expect(error).toBeInstanceOf(ProviderError);
138+
expect(error.tryNextLink).toBe(true);
139+
}
140+
});
117141
});
118142
});

0 commit comments

Comments
 (0)