Skip to content

Commit

Permalink
feat(credential-providers): expose node.js default credential provide…
Browse files Browse the repository at this point in the history
…r 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
  • Loading branch information
AllanZhengYP committed May 9, 2022
1 parent 91f13da commit 51aaffc
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 57 deletions.
2 changes: 1 addition & 1 deletion UPGRADING.md
Expand Up @@ -208,7 +208,7 @@ Default credential provider is how SDK resolve the AWS credential if you DO NOT

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

- **v3**: [defaultProvider](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_provider_node.html#defaultprovider)
- **v3**: [defaultProvider](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_credential_providers#fromnodejsproviderchain-1)
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/).

### Temporary Credentials
Expand Down
12 changes: 12 additions & 0 deletions packages/credential-provider-node/README.md
Expand Up @@ -45,6 +45,18 @@ const provider = defaultProvider({
const client = new S3Client({ credentialDefaultProvider: provider });
```

_IMPORTANT_: We provide a wrapper of this provider in `@aws-sdk/credential-providers`
package to save you from importing `getDefaultRoleAssumerWithWebIdentity()` or
`getDefaultRoleAssume()` from STS package. Similarly, you can do:

```js
const { fromNodeProviderChain } = require("@aws-sdk/credential-providers");

const credentials = fromNodeProviderChain();

const client = new S3Client({ credentials });
```

## Supported configuration

You may customize how credentials are resolved by providing an options hash to
Expand Down
20 changes: 10 additions & 10 deletions packages/credential-provider-node/src/defaultProvider.ts
Expand Up @@ -10,6 +10,8 @@ import { Credentials, MemoizedProvider } from "@aws-sdk/types";

import { remoteProvider } from "./remoteProvider";

export type DefaultProviderInit = FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit;

/**
* Creates a credential provider that will attempt to find credentials from the
* following sources (listed in order of precedence):
Expand All @@ -29,24 +31,22 @@ import { remoteProvider } from "./remoteProvider";
* @param init Configuration that is passed to each individual
* provider
*
* @see fromEnv The function used to source credentials from
* @see {@link fromEnv} The function used to source credentials from
* environment variables
* @see fromSSO The function used to source credentials from
* @see {@link fromSSO} The function used to source credentials from
* resolved SSO token cache
* @see fromTokenFile The function used to source credentials from
* @see {@link fromTokenFile} The function used to source credentials from
* token file
* @see fromIni The function used to source credentials from INI
* @see {@link fromIni} The function used to source credentials from INI
* files
* @see fromProcess The function used to sources credentials from
* @see {@link fromProcess} The function used to sources credentials from
* credential_process in INI files
* @see fromInstanceMetadata The function used to source credentials from the
* @see {@link fromInstanceMetadata} The function used to source credentials from the
* EC2 Instance Metadata Service
* @see fromContainerMetadata The function used to source credentials from the
* @see {@link fromContainerMetadata} The function used to source credentials from the
* ECS Container Metadata Service
*/
export const defaultProvider = (
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit = {}
): MemoizedProvider<Credentials> =>
export const defaultProvider = (init: DefaultProviderInit = {}): MemoizedProvider<Credentials> =>
memoize(
chain(
...(init.profile || process.env[ENV_PROFILE] ? [] : [fromEnv()]),
Expand Down
47 changes: 41 additions & 6 deletions packages/credential-providers/README.md
Expand Up @@ -23,6 +23,7 @@ A collection of all credential providers, with default clients.
1. [Supported Configuration](#supported-configuration)
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)

## `fromCognitoIdentity()`

Expand Down Expand Up @@ -119,7 +120,7 @@ const client = new FooClient({
// Optional. The master credentials used to get and refresh temporary credentials from AWS STS.
// If skipped, it uses the default credential resolved by internal STS client.
masterCredentials: fromTemporaryCredentials({
params: { RoleArn: "arn:aws:iam::1234567890:role/RoleA" }
params: { RoleArn: "arn:aws:iam::1234567890:role/RoleA" },
}),
// Required. Options passed to STS AssumeRole operation.
params: {
Expand All @@ -129,16 +130,16 @@ const client = new FooClient({
// session name with prefix of 'aws-sdk-js-'.
RoleSessionName: "aws-sdk-js-123",
// Optional. The duration, in seconds, of the role session.
DurationSeconds: 3600
DurationSeconds: 3600,
// ... For more options see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
},
// Optional. Custom STS client configurations overriding the default ones.
clientConfig: { region },
// Optional. A function that returns a promise fulfilled with an MFA token code for the provided
// MFA Serial code. Required if `params` has `SerialNumber` config.
mfaCodeProvider: async mfaSerial => {
return "token"
}
mfaCodeProvider: async (mfaSerial) => {
return "token";
},
}),
});
```
Expand Down Expand Up @@ -593,7 +594,7 @@ Successfully signed out of all SSO profiles.
### Sample files
This credential provider is only applicable if the profile specified in shared configuration and
credentials files contain ALL of the following entries:
credentials files contain ALL of the following entries.
#### `~/.aws/credentials`
Expand All @@ -615,6 +616,40 @@ sso_role_name = SampleRole
sso_start_url = https://d-abc123.awsapps.com/start
```
## `fromNodeProviderChain()`
The credential provider used as default in the Node.js clients, but with default role assumers so
you don't need to import them from STS client and supply them manually. You normally don't need
to use this explicitly in the client constructor. It is useful for utility functions requiring
credentials like S3 presigner, or RDS signer.
This credential provider will attempt to find credentials from the following sources (listed in
order of precedence):
- [Environment variables exposed via `process.env`](#fromenv)
- [SSO credentials from token cache](#fromsso)
- [Web identity token credentials](#fromtokenfile)
- [Shared credentials and config ini files](#fromini)
- [The EC2/ECS Instance Metadata Service](#fromcontainermetadata-and-frominstancemetadata)
This 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 process finds values defined via the `AWS_ACCESS_KEY_ID` and
`AWS_SECRET_ACCESS_KEY` environment variables, the files at
`~/.aws/credentials` and `~/.aws/config` will not be read, nor will any
messages be sent to the Instance Metadata Service
```js
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; // ES6 import
// const { fromNodeProviderChain } = require("@aws-sdk/credential-providers") // CommonJS import
const credentialProvider = fromNodeProviderChain({
//...any input of fromEnv(), fromSSO(), fromTokenFile(), fromIni(),
// fromProcess(), fromInstanceMetadata(), fromContainerMetadata()
// Optional. Custom STS client configurations overriding the default ones.
clientConfig: { region },
});
```
[getcredentialsforidentity_api]: https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html
[getid_api]: https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetId.html
[assumerole_api]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
Expand Down
1 change: 1 addition & 0 deletions packages/credential-providers/package.json
Expand Up @@ -33,6 +33,7 @@
"@aws-sdk/credential-provider-env": "*",
"@aws-sdk/credential-provider-imds": "*",
"@aws-sdk/credential-provider-ini": "*",
"@aws-sdk/credential-provider-node": "*",
"@aws-sdk/credential-provider-process": "*",
"@aws-sdk/credential-provider-sso": "*",
"@aws-sdk/credential-provider-web-identity": "*",
Expand Down
10 changes: 5 additions & 5 deletions packages/credential-providers/src/fromCognitoIdentity.spec.ts
@@ -1,14 +1,14 @@
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
import { fromCognitoIdentity as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";

import { fromCognitoIdentity } from "./fromCognitoIdentity";

jest.mock("@aws-sdk/client-cognito-identity", () => ({
CognitoIdentityClient: jest.fn().mockImplementation(function () {
return "COGNITO_IDENTITY_CLIENT";
}),
}));

import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
import { fromCognitoIdentity as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";

import { fromCognitoIdentity } from "./fromCognitoIdentity";

jest.mock("@aws-sdk/credential-provider-cognito-identity", () => ({
fromCognitoIdentity: jest.fn(),
}));
Expand Down
@@ -1,14 +1,14 @@
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
import { fromCognitoIdentityPool as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";

import { fromCognitoIdentityPool } from "./fromCognitoIdentityPool";

jest.mock("@aws-sdk/client-cognito-identity", () => ({
CognitoIdentityClient: jest.fn().mockImplementation(function () {
return "COGNITO_IDENTITY_CLIENT";
}),
}));

import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
import { fromCognitoIdentityPool as coreProvider } from "@aws-sdk/credential-provider-cognito-identity";

import { fromCognitoIdentityPool } from "./fromCognitoIdentityPool";

jest.mock("@aws-sdk/credential-provider-cognito-identity", () => ({
fromCognitoIdentityPool: jest.fn(),
}));
Expand Down
20 changes: 10 additions & 10 deletions packages/credential-providers/src/fromIni.spec.ts
@@ -1,16 +1,16 @@
const ROLE_ASSUMER = "ROLE_ASSUMER";
const ROLE_ASSUMER_WITH_WEB_IDENTITY = "ROLE_ASSUMER_WITH_WEB_IDENTITY";

jest.mock("@aws-sdk/client-sts", () => ({
getDefaultRoleAssumer: jest.fn().mockReturnValue(ROLE_ASSUMER),
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockReturnValue(ROLE_ASSUMER_WITH_WEB_IDENTITY),
}));

import { getDefaultRoleAssumer, getDefaultRoleAssumerWithWebIdentity } from "@aws-sdk/client-sts";
import { fromIni as coreProvider } from "@aws-sdk/credential-provider-ini";

import { fromIni } from "./fromIni";

const mockRoleAssumer = jest.fn().mockResolvedValue("ROLE_ASSUMER");
const mockRoleAssumerWithWebIdentity = jest.fn().mockResolvedValue("ROLE_ASSUMER_WITH_WEB_IDENTITY");

jest.mock("@aws-sdk/client-sts", () => ({
getDefaultRoleAssumer: jest.fn().mockImplementation(() => mockRoleAssumer),
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockImplementation(() => mockRoleAssumerWithWebIdentity),
}));

jest.mock("@aws-sdk/credential-provider-ini", () => ({
fromIni: jest.fn(),
}));
Expand All @@ -25,8 +25,8 @@ describe("fromIni", () => {
fromIni({ profile });
expect(coreProvider).toBeCalledWith({
profile,
roleAssumer: ROLE_ASSUMER,
roleAssumerWithWebIdentity: ROLE_ASSUMER_WITH_WEB_IDENTITY,
roleAssumer: mockRoleAssumer,
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
});
expect(getDefaultRoleAssumer).toBeCalled();
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalled();
Expand Down
58 changes: 58 additions & 0 deletions packages/credential-providers/src/fromNodeProviderChain.spec.ts
@@ -0,0 +1,58 @@
import { getDefaultRoleAssumer, getDefaultRoleAssumerWithWebIdentity } from "@aws-sdk/client-sts";
import { defaultProvider } from "@aws-sdk/credential-provider-node";

import { fromNodeProviderChain } from "./fromNodeProviderChain";

const mockRoleAssumer = jest.fn().mockResolvedValue("ROLE_ASSUMER");
const mockRoleAssumerWithWebIdentity = jest.fn().mockResolvedValue("ROLE_ASSUMER_WITH_WEB_IDENTITY");

jest.mock("@aws-sdk/client-sts", () => ({
getDefaultRoleAssumer: jest.fn().mockImplementation(() => mockRoleAssumer),
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockImplementation(() => mockRoleAssumerWithWebIdentity),
}));

jest.mock("@aws-sdk/credential-provider-node", () => ({
defaultProvider: jest.fn(),
}));

describe(fromNodeProviderChain.name, () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("should inject default role assumers", () => {
const profile = "profile";
fromNodeProviderChain({ profile });
expect(defaultProvider).toBeCalledWith({
profile,
roleAssumer: mockRoleAssumer,
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
});
expect(getDefaultRoleAssumer).toBeCalled();
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalled();
});

it("should use supplied role assumers", () => {
const profile = "profile";
const roleAssumer = jest.fn();
const roleAssumerWithWebIdentity = jest.fn();
fromNodeProviderChain({ profile, roleAssumer, roleAssumerWithWebIdentity });
expect(defaultProvider).toBeCalledWith({
profile,
roleAssumer,
roleAssumerWithWebIdentity,
});
expect(getDefaultRoleAssumer).not.toBeCalled();
expect(getDefaultRoleAssumerWithWebIdentity).not.toBeCalled();
});

it("should use supplied sts options", () => {
const profile = "profile";
const clientConfig = {
region: "US_BAR_1",
};
fromNodeProviderChain({ profile, clientConfig });
expect(getDefaultRoleAssumer).toBeCalledWith(clientConfig);
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalledWith(clientConfig);
});
});
37 changes: 37 additions & 0 deletions packages/credential-providers/src/fromNodeProviderChain.ts
@@ -0,0 +1,37 @@
import { getDefaultRoleAssumer, getDefaultRoleAssumerWithWebIdentity, STSClientConfig } from "@aws-sdk/client-sts";
import { defaultProvider, DefaultProviderInit } from "@aws-sdk/credential-provider-node";
import { CredentialProvider } from "@aws-sdk/types";

export interface fromNodeProviderChainInit extends DefaultProviderInit {
clientConfig?: STSClientConfig;
}

/**
* This is the same credential provider as {@link defaultProvider|the default provider for Node.js SDK},
* but with default role assumers so you don't need to import them from
* STS client and supply them manually.
*
* You normally don't need to use this explicitly in the client constructor.
* It is useful for utility functions requiring credentials like S3 presigner,
* or RDS signer.
*
* ```js
* import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; // ES6 import
* // const { fromNodeProviderChain } = require("@aws-sdk/credential-providers") // CommonJS import
*
* const credentialProvider = fromNodeProviderChain({
* //...any input of fromEnv(), fromSSO(), fromTokenFile(), fromIni(),
* // fromProcess(), fromInstanceMetadata(), fromContainerMetadata()
*
* // Optional. Custom STS client configurations overriding the default ones.
* clientConfig: { region },
* })
* ```
*/
export const fromNodeProviderChain = (init: fromNodeProviderChainInit = {}): CredentialProvider =>
defaultProvider({
...init,
roleAssumer: init.roleAssumer ?? getDefaultRoleAssumer(init.clientConfig),
roleAssumerWithWebIdentity:
init.roleAssumerWithWebIdentity ?? getDefaultRoleAssumerWithWebIdentity(init.clientConfig),
});
@@ -1,3 +1,7 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";

import { fromTemporaryCredentials } from "./fromTemporaryCredentials";

const sendMock = jest.fn();
jest.mock("@aws-sdk/client-sts", () => ({
STSClient: jest.fn().mockImplementation((config) => ({
Expand All @@ -17,10 +21,6 @@ jest.mock("@aws-sdk/client-sts", () => ({
}),
}));

import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";

import { fromTemporaryCredentials } from "./fromTemporaryCredentials";

describe("fromTemporaryCredentials", () => {
const RoleArn = "ROLE_ARN";
const RoleSessionName = "ROLE_SESSION_NAME";
Expand Down
16 changes: 8 additions & 8 deletions packages/credential-providers/src/fromTokenFile.spec.ts
@@ -1,14 +1,14 @@
const ROLE_ASSUMER_WITH_WEB_IDENTITY = "ROLE_ASSUMER_WITH_WEB_IDENTITY";

jest.mock("@aws-sdk/client-sts", () => ({
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockReturnValue(ROLE_ASSUMER_WITH_WEB_IDENTITY),
}));

import { getDefaultRoleAssumerWithWebIdentity } from "@aws-sdk/client-sts";
import { fromTokenFile as coreProvider } from "@aws-sdk/credential-provider-web-identity";

import { fromTokenFile } from "./fromTokenFile";

const mockRoleAssumerWithWebIdentity = jest.fn().mockResolvedValue("ROLE_ASSUMER_WITH_WEB_IDENTITY");

jest.mock("@aws-sdk/client-sts", () => ({
getDefaultRoleAssumerWithWebIdentity: jest.fn().mockImplementation(() => mockRoleAssumerWithWebIdentity),
}));

jest.mock("@aws-sdk/credential-provider-web-identity", () => ({
fromTokenFile: jest.fn(),
}));
Expand All @@ -21,7 +21,7 @@ describe("fromTokenFile", () => {
it("should inject default role assumer", () => {
fromTokenFile();
expect(coreProvider).toBeCalledWith({
roleAssumerWithWebIdentity: ROLE_ASSUMER_WITH_WEB_IDENTITY,
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
});
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalled();
});
Expand All @@ -34,7 +34,7 @@ describe("fromTokenFile", () => {
clientConfig,
});
expect((coreProvider as jest.Mock).mock.calls[0][0]).toMatchObject({
roleAssumerWithWebIdentity: ROLE_ASSUMER_WITH_WEB_IDENTITY,
roleAssumerWithWebIdentity: mockRoleAssumerWithWebIdentity,
});
expect(getDefaultRoleAssumerWithWebIdentity).toBeCalledWith(clientConfig);
});
Expand Down

0 comments on commit 51aaffc

Please sign in to comment.