Skip to content

Commit 85597fd

Browse files
AllanZhengYPtrivikr
authored andcommitted
feat(credential-provider-web-identity): support web federated identity (#2203)
Co-authored-by: Trivikram Kamat <[email protected]>
1 parent 14a6a77 commit 85597fd

File tree

6 files changed

+395
-183
lines changed

6 files changed

+395
-183
lines changed

packages/credential-provider-web-identity/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,86 @@
77

88
This module includes functions which get credentials by calling STS assumeRoleWithWebIdentity API.
99

10+
## fromWebToken
11+
12+
The function `fromWebToken` returns `CredentialProvider` that get credentials calling sts:assumeRoleWithWebIdentity
13+
API via `roleAssumerWithWebIdentity`.
14+
15+
### Supported configuration
16+
17+
This configuration supports all the input parameters from
18+
[sts:AssumeWithWebIdentity](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-sts/modules/assumerolewithwebidentityrequest.html) API. The following options are supported:
19+
20+
- `roleArn` - The Amazon Resource Name (ARN) of the role that the caller is assuming.
21+
- `webIdentityToken` - The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.
22+
- `roleSessionName` - An identifier for the assumed role session.
23+
- `providerId` - The fully qualified host component of the domain name of the identity provider. Do not specify this
24+
value for OpenID Connect ID tokens.
25+
- `policyArns` - The Amazon Resource Names (ARNs) of the IAM managed policies that you want to use as managed session
26+
policies.
27+
- `policy` - An IAM policy in JSON format that you want to use as an inline session policy.
28+
- `durationSeconds` - The duration, in seconds, of the role session. Default to 3600.
29+
- `roleAssumerWithWebIdentity` - A function that assumes a role with web identity
30+
and returns a promise fulfilled with credentials for the assumed role. You may call
31+
`sts:assumeRoleWithWebIdentity` API within this function.
32+
33+
### Examples
34+
35+
You can directly configure individual identity providers to access AWS resources using web identity federation. AWS
36+
currently supports authenticating users using web identity federation through several identity providers:
37+
38+
- [Login with Amazon](https://login.amazon.com/)
39+
40+
- [Facebook Login](https://developers.facebook.com/docs/facebook-login/web/)
41+
42+
- [Google Sign-in](https://developers.google.com/identity/)
43+
44+
You must first register your application with the providers that your application supports. Next, create an IAM role and
45+
set up permissions for it. The IAM role you create is then used to grant the permissions you configured for it through
46+
the respective identity provider. For example, you can set up a role that allows users who logged in through Facebook
47+
to have read access to a specific Amazon S3 bucket you control.
48+
49+
After you have both an IAM role with configured privileges and an application registered with your chosen identity
50+
providers, you can set up the SDK to get credentials for the IAM role using helper code, as follows:
51+
52+
```javascript
53+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
54+
import { STSClient, AssumeRoleWithWebIdentityCommand } from "@aws-sdk/client-sts";
55+
import { fromWebToken } from "@aws-sdk/credential-provider-web-identity";
56+
57+
const stsClient = new STSClient({});
58+
59+
const roleAssumerWithWebIdentity = async (params) => {
60+
const { Credentials } = await stsClient.send(
61+
new AssumeRoleWithWebIdentityCommand(params)
62+
);
63+
if (!Credentials || !Credentials.AccessKeyId || !Credentials.SecretAccessKey) {
64+
throw new Error(`Invalid response from STS.assumeRole call with role ${params.RoleArn}`);
65+
}
66+
return {
67+
accessKeyId: Credentials.AccessKeyId,
68+
secretAccessKey: Credentials.SecretAccessKey,
69+
sessionToken: Credentials.SessionToken,
70+
expiration: Credentials.Expiration,
71+
};
72+
};
73+
74+
const dynamodb = new DynamoDBClient({
75+
region,
76+
credentials: fromWebToken({
77+
roleArn: 'arn:aws:iam::<AWS_ACCOUNT_ID>/:role/<WEB_IDENTITY_ROLE_NAME>',
78+
providerId: 'graph.facebook.com|www.amazon.com', // this is null for Google
79+
webIdentityToken: ACCESS_TOKEN // from OpenID token identity provider
80+
roleAssumerWithWebIdentity,
81+
})
82+
});
83+
84+
```
85+
86+
The value in the ProviderId parameter depends on the specified identity provider. The value in the WebIdentityToken
87+
parameter is the access token retrieved from a successful login with the identity provider. For more information on how
88+
to configure and retrieve access tokens for each identity provider, see the documentation for the identity provider.
89+
1090
## fromTokenFile
1191

1292
The function `fromTokenFile` returns `CredentialProvider` that reads credentials as follows:
Lines changed: 60 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { ProviderError } from "@aws-sdk/property-provider";
21
import { readFileSync } from "fs";
3-
4-
import { AssumeRoleWithWebIdentityParams, fromTokenFile, FromTokenFileInit } from "./fromTokenFile";
2+
jest.mock("./fromWebToken", () => ({
3+
fromWebToken: jest.fn().mockReturnValue(() => Promise.resolve(MOCK_CREDS)),
4+
}));
5+
import { fromTokenFile } from "./fromTokenFile";
6+
import { fromWebToken } from "./fromWebToken";
57

68
const ENV_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
79
const ENV_ROLE_ARN = "AWS_ROLE_ARN";
@@ -30,57 +32,6 @@ describe(fromTokenFile.name, () => {
3032
jest.restoreAllMocks();
3133
});
3234

33-
const testRoleAssumerWithWebIdentityNotDefined = async (init: FromTokenFileInit, roleArn: string) => {
34-
try {
35-
// @ts-ignore An argument for 'init' was not provided.
36-
await fromTokenFile(init)();
37-
fail(`Expected error to be thrown`);
38-
} catch (error) {
39-
expect(error).toEqual(
40-
new ProviderError(
41-
`Role Arn '${roleArn}' needs to be assumed with web identity, but no role assumption callback was provided.`,
42-
false
43-
)
44-
);
45-
}
46-
};
47-
48-
const testReadFileSyncError = async (init: FromTokenFileInit) => {
49-
const readFileSyncError = new Error("readFileSyncError");
50-
(readFileSync as jest.Mock).mockImplementation(() => {
51-
throw readFileSyncError;
52-
});
53-
try {
54-
await fromTokenFile(init)();
55-
fail(`Expected error to be thrown`);
56-
} catch (error) {
57-
expect(error).toEqual(readFileSyncError);
58-
}
59-
expect(readFileSync).toHaveBeenCalledTimes(1);
60-
};
61-
62-
const testRoleAssumerWithWebIdentitySuccess = async (init: FromTokenFileInit) => {
63-
const creds = await fromTokenFile(init)();
64-
expect(creds).toEqual(MOCK_CREDS);
65-
expect(readFileSync).toHaveBeenCalledTimes(1);
66-
expect(readFileSync).toHaveBeenCalledWith(mockTokenFile, { encoding: "ascii" });
67-
};
68-
69-
const testRandomValueForRoleSessionName = async (init: FromTokenFileInit) => {
70-
const mockDateNow = Date.now();
71-
const spyDateNow = jest.spyOn(Date, "now").mockReturnValueOnce(mockDateNow);
72-
73-
const creds = await fromTokenFile({
74-
...init,
75-
roleAssumerWithWebIdentity: async (params: AssumeRoleWithWebIdentityParams) => {
76-
expect(params.RoleSessionName).toEqual(`aws-sdk-js-session-${mockDateNow}`);
77-
return MOCK_CREDS;
78-
},
79-
})();
80-
expect(creds).toEqual(MOCK_CREDS);
81-
expect(spyDateNow).toHaveBeenCalledTimes(1);
82-
};
83-
8435
describe("reads config from env", () => {
8536
const original_ENV_TOKEN_FILE = process.env[ENV_TOKEN_FILE];
8637
const original_ENV_ROLE_ARN = process.env[ENV_ROLE_ARN];
@@ -98,83 +49,70 @@ describe(fromTokenFile.name, () => {
9849
process.env[ENV_ROLE_SESSION_NAME] = original_ENV_ROLE_SESSION_NAME;
9950
});
10051

101-
it("throws if roleAssumerWithWebIdentity is not defined", async () => {
102-
return testRoleAssumerWithWebIdentityNotDefined({}, process.env[ENV_ROLE_ARN]);
52+
it(`passes values to ${fromWebToken.name}`, async () => {
53+
const roleAssumerWithWebIdentity = jest.fn();
54+
const creds = await fromTokenFile({
55+
roleAssumerWithWebIdentity,
56+
})();
57+
expect(creds).toEqual(MOCK_CREDS);
58+
expect(fromWebToken as jest.Mock).toBeCalledTimes(1);
59+
const webTokenInit = (fromWebToken as jest.Mock).mock.calls[0][0];
60+
expect(webTokenInit.webIdentityToken).toBe(mockTokenValue);
61+
expect(webTokenInit.roleSessionName).toBe(mockRoleSessionName);
62+
expect(webTokenInit.roleArn).toBe(mockRoleArn);
63+
expect(webTokenInit.roleAssumerWithWebIdentity).toBe(roleAssumerWithWebIdentity);
10364
});
10465

105-
it("throws if ENV_TOKEN_FILE read from disk failed", async () => {
106-
return testReadFileSyncError({
107-
roleAssumerWithWebIdentity: async (params: AssumeRoleWithWebIdentityParams) => {
108-
return MOCK_CREDS;
109-
},
110-
});
66+
it("prefers init parameters over environmental variables", async () => {
67+
const roleAssumerWithWebIdentity = jest.fn();
68+
const init = {
69+
webIdentityTokenFile: "anotherTokenFile",
70+
roleArn: "anotherRoleArn",
71+
roleSessionName: "anotherRoleSessionName",
72+
roleAssumerWithWebIdentity,
73+
};
74+
const creds = await fromTokenFile(init)();
75+
expect(creds).toEqual(MOCK_CREDS);
76+
expect(fromWebToken as jest.Mock).toBeCalledTimes(1);
77+
const webTokenInit = (fromWebToken as jest.Mock).mock.calls[0][0];
78+
expect(webTokenInit.roleSessionName).toBe(init.roleSessionName);
79+
expect(webTokenInit.roleArn).toBe(init.roleArn);
80+
expect(webTokenInit.roleAssumerWithWebIdentity).toBe(roleAssumerWithWebIdentity);
81+
expect(readFileSync as jest.Mock).toBeCalledTimes(1);
82+
expect((readFileSync as jest.Mock).mock.calls[0][0]).toBe(init.webIdentityTokenFile);
11183
});
11284

113-
it("passes values to roleAssumerWithWebIdentity", async () => {
114-
return testRoleAssumerWithWebIdentitySuccess({
115-
roleAssumerWithWebIdentity: async (params: AssumeRoleWithWebIdentityParams) => {
116-
expect(params.WebIdentityToken).toEqual(mockTokenValue);
117-
expect(params.RoleArn).toEqual(mockRoleArn);
118-
expect(params.RoleSessionName).toEqual(mockRoleSessionName);
119-
return MOCK_CREDS;
120-
},
85+
it("throws if ENV_TOKEN_FILE read from disk failed", async () => {
86+
const readFileSyncError = new Error("readFileSyncError");
87+
(readFileSync as jest.Mock).mockImplementation(() => {
88+
throw readFileSyncError;
12189
});
122-
});
123-
124-
it("generates a random value for RoleSessionName if not available", async () => {
125-
delete process.env[ENV_ROLE_SESSION_NAME];
126-
return testRandomValueForRoleSessionName({});
127-
});
128-
});
129-
130-
describe("reads config from configuration keys", () => {
131-
const original_ENV_TOKEN_FILE = process.env[ENV_TOKEN_FILE];
132-
const original_ENV_ROLE_ARN = process.env[ENV_ROLE_ARN];
133-
const original_ENV_ROLE_SESSION_NAME = process.env[ENV_ROLE_SESSION_NAME];
134-
135-
beforeAll(() => {
136-
delete process.env[ENV_TOKEN_FILE];
137-
delete process.env[ENV_ROLE_ARN];
138-
delete process.env[ENV_ROLE_SESSION_NAME];
139-
});
140-
141-
afterAll(() => {
142-
process.env[ENV_TOKEN_FILE] = original_ENV_TOKEN_FILE;
143-
process.env[ENV_ROLE_ARN] = original_ENV_ROLE_ARN;
144-
process.env[ENV_ROLE_SESSION_NAME] = original_ENV_ROLE_SESSION_NAME;
145-
});
146-
147-
it("throws if roleAssumerWithWebIdentity is not defined", async () => {
148-
return testRoleAssumerWithWebIdentityNotDefined({ roleArn: mockRoleArn }, mockRoleArn);
90+
try {
91+
await fromTokenFile({ roleAssumerWithWebIdentity: jest.fn() })();
92+
fail(`Expected error to be thrown`);
93+
} catch (error) {
94+
expect(error).toEqual(readFileSyncError);
95+
}
96+
expect(readFileSync).toHaveBeenCalledTimes(1);
14997
});
15098

15199
it("throws if web_identity_token_file read from disk failed", async () => {
152-
return testReadFileSyncError({
153-
webIdentityTokenFile: mockTokenFile,
154-
roleArn: mockRoleArn,
155-
roleSessionName: mockRoleSessionName,
156-
roleAssumerWithWebIdentity: async (params: AssumeRoleWithWebIdentityParams) => {
157-
return MOCK_CREDS;
158-
},
159-
});
160-
});
161-
162-
it("passes values to roleAssumerWithWebIdentity", async () => {
163-
return testRoleAssumerWithWebIdentitySuccess({
164-
webIdentityTokenFile: mockTokenFile,
165-
roleArn: mockRoleArn,
166-
roleSessionName: mockRoleSessionName,
167-
roleAssumerWithWebIdentity: async (params: AssumeRoleWithWebIdentityParams) => {
168-
expect(params.WebIdentityToken).toEqual(mockTokenValue);
169-
expect(params.RoleArn).toEqual(mockRoleArn);
170-
expect(params.RoleSessionName).toEqual(mockRoleSessionName);
171-
return MOCK_CREDS;
172-
},
100+
const readFileSyncError = new Error("readFileSyncError");
101+
(readFileSync as jest.Mock).mockImplementation(() => {
102+
throw readFileSyncError;
173103
});
174-
});
175-
176-
it("generates a random value for RoleSessionName if not available", async () => {
177-
return testRandomValueForRoleSessionName({ webIdentityTokenFile: mockTokenFile, roleArn: mockRoleArn });
104+
try {
105+
await fromTokenFile({
106+
webIdentityTokenFile: mockTokenFile,
107+
roleArn: mockRoleArn,
108+
roleSessionName: mockRoleSessionName,
109+
roleAssumerWithWebIdentity: jest.fn(),
110+
})();
111+
fail(`Expected error to be thrown`);
112+
} catch (error) {
113+
expect(error).toEqual(readFileSyncError);
114+
}
115+
expect(readFileSync).toHaveBeenCalledTimes(1);
178116
});
179117
});
180118
});
Lines changed: 12 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,29 @@
1-
import { ProviderError } from "@aws-sdk/property-provider";
2-
import { CredentialProvider, Credentials } from "@aws-sdk/types";
1+
import { CredentialProvider } from "@aws-sdk/types";
32
import { readFileSync } from "fs";
43

4+
import { fromWebToken, FromWebTokenInit } from "./fromWebToken";
5+
56
const ENV_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";
67
const ENV_ROLE_ARN = "AWS_ROLE_ARN";
78
const ENV_ROLE_SESSION_NAME = "AWS_ROLE_SESSION_NAME";
89

9-
export interface AssumeRoleWithWebIdentityParams {
10-
/**
11-
* <p>The Amazon Resource Name (ARN) of the role that the caller is assuming.</p>
12-
*/
13-
RoleArn: string;
14-
/**
15-
* <p>An identifier for the assumed role session. Typically, you pass the name or identifier
16-
* that is associated with the user who is using your application. That way, the temporary
17-
* security credentials that your application will use are associated with that user. This
18-
* session name is included as part of the ARN and assumed role ID in the
19-
* <code>AssumedRoleUser</code> response element.</p>
20-
* <p>The regex used to validate this parameter is a string of characters
21-
* consisting of upper- and lower-case alphanumeric characters with no spaces. You can
22-
* also include underscores or any of the following characters: =,.@-</p>
23-
*/
24-
RoleSessionName: string;
25-
/**
26-
* <p>The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity
27-
* provider. Your application must get this token by authenticating the user who is using your
28-
* application with a web identity provider before the application makes an
29-
* <code>AssumeRoleWithWebIdentity</code> call. </p>
30-
*/
31-
WebIdentityToken: string;
32-
}
33-
export interface FromTokenFileInit {
10+
export interface FromTokenFileInit extends Partial<Omit<FromWebTokenInit, "webIdentityToken">> {
3411
/**
3512
* File location of where the `OIDC` token is stored.
3613
*/
3714
webIdentityTokenFile?: string;
38-
39-
/**
40-
* The IAM role wanting to be assumed.
41-
*/
42-
roleArn?: string;
43-
44-
/**
45-
* The IAM session name used to distinguish sessions.
46-
*/
47-
roleSessionName?: string;
48-
49-
/**
50-
* A function that assumes a role with web identity and returns a promise fulfilled with
51-
* credentials for the assumed role.
52-
*
53-
* @param sourceCreds The credentials with which to assume a role.
54-
* @param params
55-
*/
56-
roleAssumerWithWebIdentity?: (params: AssumeRoleWithWebIdentityParams) => Promise<Credentials>;
5715
}
5816

5917
/**
6018
* Represents OIDC credentials from a file on disk.
6119
*/
62-
export const fromTokenFile = (init: FromTokenFileInit): CredentialProvider => async () => {
63-
const { webIdentityTokenFile, roleArn, roleSessionName, roleAssumerWithWebIdentity } = init;
64-
65-
if (!roleAssumerWithWebIdentity) {
66-
throw new ProviderError(
67-
`Role Arn '${roleArn ?? process.env[ENV_ROLE_ARN]}' needs to be assumed with web identity,` +
68-
` but no role assumption callback was provided.`,
69-
false
70-
);
71-
}
72-
73-
return roleAssumerWithWebIdentity({
74-
WebIdentityToken: readFileSync(webIdentityTokenFile ?? process.env[ENV_TOKEN_FILE]!, { encoding: "ascii" }),
75-
RoleArn: roleArn ?? process.env[ENV_ROLE_ARN]!,
76-
RoleSessionName: roleSessionName ?? process.env[ENV_ROLE_SESSION_NAME] ?? `aws-sdk-js-session-${Date.now()}`,
20+
export const fromTokenFile = (init: FromTokenFileInit): CredentialProvider => {
21+
const { webIdentityTokenFile, roleArn, roleSessionName } = init;
22+
23+
return fromWebToken({
24+
...init,
25+
webIdentityToken: readFileSync(webIdentityTokenFile ?? process.env[ENV_TOKEN_FILE]!, { encoding: "ascii" }),
26+
roleArn: roleArn ?? process.env[ENV_ROLE_ARN]!,
27+
roleSessionName: roleSessionName ?? process.env[ENV_ROLE_SESSION_NAME],
7728
});
7829
};

0 commit comments

Comments
 (0)