Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@
],
"**/*.{ts,js,md,json}": "prettier --write"
}
}
}
35 changes: 33 additions & 2 deletions packages/credential-provider-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
This module provides a factory function, `fromEnv`, that will attempt to source
AWS credentials from a Node.JS environment. It will attempt to find credentials
from the following sources (listed in order of precedence):
_ Environment variables exposed via `process.env`
_ Shared credentials and config ini files \* The EC2/ECS Instance Metadata Service

- Environment variables exposed via `process.env`
- SSO credentials from token cache
- Web identity token credentials
- Shared credentials and config ini files
- The EC2/ECS Instance Metadata Service

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

_IMPORTANT_: if you intend for your code to run using EKS roles at some point
(for example in a production environment, but not when working locally) then
you must explicitly specify a value for `roleAssumerWithWebIdentity`. There is a
default function available in `@aws-sdk/client-sts` package. An example of using
this:

```js
const { getDefaultRoleAssumerWithWebIdentity } = require("@aws-sdk/client-sts");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");

const provider = defaultProvider({
roleAssumerWithWebIdentity: getDefaultRoleAssumerWithWebIdentity,
});

const client = new S3Client({ credentialDefaultProvider: provider });
```

## Supported configuration

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

- [AWS Credential Provider for Node.JS - Environment Variables](../credential-provider-env)
- [AWS Credential Provider for Node.JS - SSO](../credential-provider-sso)
- [AWS Credential Provider for Node.JS - Web Identity](../credential-provider-web-identity)
- [AWS Credential Provider for Node.JS - Shared Configuration Files](../credential-provider-ini)
- [AWS Credential Provider for Node.JS - Instance and Container Metadata](../credential-provider-imds)
- [AWS Shared Configuration File Loader](../shared-ini-file-loader)
1 change: 1 addition & 0 deletions packages/credential-provider-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "3.13.1",
"@aws-sdk/credential-provider-web-identity": "3.13.1",
"@aws-sdk/credential-provider-imds": "3.13.1",
"@aws-sdk/credential-provider-ini": "3.13.1",
"@aws-sdk/credential-provider-process": "3.13.1",
Expand Down
70 changes: 70 additions & 0 deletions packages/credential-provider-node/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ jest.mock("@aws-sdk/credential-provider-sso", () => {
});
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";

jest.mock("@aws-sdk/credential-provider-web-identity", () => {
const webIdentityProvider = jest.fn();
return {
fromTokenFile: jest.fn().mockReturnValue(webIdentityProvider),
};
});
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";

jest.mock("@aws-sdk/credential-provider-ini", () => {
const iniProvider = jest.fn();
return {
Expand Down Expand Up @@ -129,6 +137,29 @@ describe("defaultProvider", () => {
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromProcess() as any).mock.calls.length).toBe(0);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});

it("should stop after the Web Identity provider if credentials have been found", async () => {
const creds = {
accessKeyId: "foo",
secretAccessKey: "bar",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromTokenFile() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});
Expand All @@ -141,12 +172,14 @@ describe("defaultProvider", () => {

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromProcess() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
Expand All @@ -168,6 +201,7 @@ describe("defaultProvider", () => {
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});
Expand All @@ -180,13 +214,15 @@ describe("defaultProvider", () => {
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));

expect(await defaultProvider()()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(1);
Expand All @@ -201,6 +237,7 @@ describe("defaultProvider", () => {
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));

Expand All @@ -220,6 +257,7 @@ describe("defaultProvider", () => {
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.resolve(creds));
Expand All @@ -230,6 +268,7 @@ describe("defaultProvider", () => {
expect((fromEnv() as any).mock.calls.length).toBe(1);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
Expand All @@ -244,13 +283,15 @@ describe("defaultProvider", () => {
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));

await expect(defaultProvider()()).resolves;
expect((loadSharedConfigFiles as any).mock.calls.length).toBe(1);
expect((fromIni as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
expect((fromSSO as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
expect((fromTokenFile as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
expect((fromProcess as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
});

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

it("should pass configuration on to the Web Identity provider", async () => {
const webIdentityConfig: FromTokenFileInit = {
roleArn: "someRoleArn",
webIdentityTokenFile: "/home/user/.secrets/tokenFile",
};

(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
(fromTokenFile() as any).mockImplementation(() =>
Promise.resolve({
accessKeyId: "foo",
secretAccessKey: "bar",
})
);

(fromTokenFile as any).mockClear();

await expect(defaultProvider(webIdentityConfig)()).resolves;

expect((fromTokenFile as any).mock.calls.length).toBe(1);
expect((fromTokenFile as any).mock.calls[0][0]).toEqual({ ...webIdentityConfig, loadedConfig });
});

it("should pass configuration on to the ini provider", async () => {
const iniConfig: FromIniInit = {
profile: "foo",
Expand Down Expand Up @@ -443,13 +507,15 @@ describe("defaultProvider", () => {
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromSSO() as any).mockImplementation(() => Promise.resolve(Promise.resolve(creds)));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));

expect(await defaultProvider({ profile: "foo" })()).toEqual(creds);
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});
Expand All @@ -463,6 +529,7 @@ describe("defaultProvider", () => {
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromSSO() as any).mockImplementation(() => Promise.resolve(creds));
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromTokenFile() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
Expand All @@ -472,6 +539,7 @@ describe("defaultProvider", () => {
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(0);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromProcess() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
Expand All @@ -493,6 +561,7 @@ describe("defaultProvider", () => {
expect((fromEnv() as any).mock.calls.length).toBe(0);
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});
Expand All @@ -516,6 +585,7 @@ describe("defaultProvider", () => {
expect((fromSSO() as any).mock.calls.length).toBe(1);
expect((fromIni() as any).mock.calls.length).toBe(1);
expect((fromProcess() as any).mock.calls.length).toBe(1);
expect((fromTokenFile() as any).mock.calls.length).toBe(0);
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
});
Expand Down
9 changes: 7 additions & 2 deletions packages/credential-provider-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { ENV_PROFILE, fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process";
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
import { chain, memoize, ProviderError } from "@aws-sdk/property-provider";
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
import { CredentialProvider } from "@aws-sdk/types";
Expand All @@ -19,6 +20,8 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
* Creates a credential provider that will attempt to find credentials from the
* following sources (listed in order of precedence):
* * Environment variables exposed via `process.env`
* * SSO credentials from token cache
* * Web identity token credentials
* * Shared credentials and config ini files
* * The EC2/ECS Instance Metadata Service
*
Expand All @@ -36,6 +39,8 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
* environment variables
* @see fromSSO The function used to source credentials from
* resolved SSO token cache
* @see fromTokenFile The function used to source credentials from
* token file
* @see fromIni The function used to source credentials from INI
* files
* @see fromProcess The function used to sources credentials from
Expand All @@ -46,11 +51,11 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
* ECS Container Metadata Service
*/
export const defaultProvider = (
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit = {}
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit = {}
): CredentialProvider => {
const options = { profile: process.env[ENV_PROFILE], ...init };
if (!options.loadedConfig) options.loadedConfig = loadSharedConfigFiles(init);
const providers = [fromSSO(options), fromIni(options), fromProcess(options), remoteProvider(options)];
const providers = [fromSSO(options), fromIni(options), fromProcess(options), fromTokenFile(options), remoteProvider(options)];
if (!options.profile) providers.unshift(fromEnv());
const providerChain = chain(...providers);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const ENV_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
const ENV_ROLE_ARN = "AWS_ROLE_ARN";
const ENV_ROLE_SESSION_NAME = "AWS_ROLE_SESSION_NAME";

import { ProviderError } from "@aws-sdk/property-provider";

jest.mock("fs");

const MOCK_CREDS = {
Expand Down Expand Up @@ -114,5 +116,27 @@ describe(fromTokenFile.name, () => {
}
expect(readFileSync).toHaveBeenCalledTimes(1);
});

it("throws if web_identity_token_file is not specified", async () => {
try {
delete process.env[ENV_TOKEN_FILE];
await fromTokenFile()();
fail(`Expected error to be thrown`);
} catch (error) {
expect(error).toBeInstanceOf(ProviderError);
expect(error.tryNextLink).toBe(true);
}
});

it("throws if role_arn is not specified", async () => {
try {
delete process.env[ENV_ROLE_ARN];
await fromTokenFile()();
fail(`Expected error to be thrown`);
} catch (error) {
expect(error).toBeInstanceOf(ProviderError);
expect(error.tryNextLink).toBe(true);
}
});
});
});
Loading