Skip to content

Commit 1429ad1

Browse files
authored
feat(credential-provider-sso): support resolving credentials from SSO token (#2055)
* feat(credential-provider-sso): support AWS SSO credentials * feat(credential-provider-node): add sso cred to default chain * chore: nohoist client-sso Yarn install would fail with client-sso/node_modules/@type no exist if hoisted.
1 parent 8d99715 commit 1429ad1

File tree

15 files changed

+828
-22
lines changed

15 files changed

+828
-22
lines changed

Diff for: package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@
101101
"**/karma*",
102102
"**/karma*/**",
103103
"**/@types/mocha*",
104-
"**/@types/mocha*/**"
104+
"**/@types/mocha*/**",
105+
"**/@aws-sdk/client-sso/**"
105106
]
106107
},
107108
"husky": {
@@ -117,4 +118,4 @@
117118
],
118119
"**/*.{ts,js,md,json}": "prettier --write"
119120
}
120-
}
121+
}

Diff for: packages/credential-provider-node/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@aws-sdk/property-provider": "3.6.1",
3434
"@aws-sdk/shared-ini-file-loader": "3.6.1",
3535
"@aws-sdk/types": "3.6.1",
36+
"@aws-sdk/credential-provider-sso": "3.0.0",
3637
"tslib": "^1.8.0"
3738
},
3839
"devDependencies": {

Diff for: packages/credential-provider-node/src/index.spec.ts

+105-17
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ jest.mock("@aws-sdk/shared-ini-file-loader", () => ({
2121
}));
2222
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
2323

24+
jest.mock("@aws-sdk/credential-provider-sso", () => {
25+
const ssoProvider = jest.fn();
26+
return {
27+
fromSSO: jest.fn().mockReturnValue(ssoProvider),
28+
};
29+
});
30+
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
31+
2432
jest.mock("@aws-sdk/credential-provider-ini", () => {
2533
const iniProvider = jest.fn();
2634
return {
@@ -81,11 +89,13 @@ beforeEach(() => {
8189
});
8290

8391
(fromEnv() as any).mockClear();
92+
(fromSSO() as any).mockClear();
8493
(fromIni() as any).mockClear();
8594
(fromProcess() as any).mockClear();
8695
(fromContainerMetadata() as any).mockClear();
8796
(fromInstanceMetadata() as any).mockClear();
8897
(fromEnv as any).mockClear();
98+
(fromSSO as any).mockClear();
8999
(fromIni as any).mockClear();
90100
(fromProcess as any).mockClear();
91101
(fromContainerMetadata as any).mockClear();
@@ -120,17 +130,37 @@ describe("defaultProvider", () => {
120130
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
121131
});
122132

133+
it("should stop after the SSO provider if credentials have been found", async () => {
134+
const creds = {
135+
accessKeyId: "foo",
136+
secretAccessKey: "bar",
137+
};
138+
139+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
140+
(fromSSO() as any).mockImplementation(() => Promise.resolve(creds));
141+
142+
expect(await defaultProvider()()).toEqual(creds);
143+
expect((fromEnv() as any).mock.calls.length).toBe(1);
144+
expect((fromSSO() as any).mock.calls.length).toBe(1);
145+
expect((fromIni() as any).mock.calls.length).toBe(0);
146+
expect((fromProcess() as any).mock.calls.length).toBe(0);
147+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
148+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
149+
});
150+
123151
it("should stop after the ini provider if credentials have been found", async () => {
124152
const creds = {
125153
accessKeyId: "foo",
126154
secretAccessKey: "bar",
127155
};
128156

129157
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
158+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
130159
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
131160

132161
expect(await defaultProvider()()).toEqual(creds);
133162
expect((fromEnv() as any).mock.calls.length).toBe(1);
163+
expect((fromSSO() as any).mock.calls.length).toBe(1);
134164
expect((fromIni() as any).mock.calls.length).toBe(1);
135165
expect((fromProcess() as any).mock.calls.length).toBe(0);
136166
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
@@ -144,11 +174,13 @@ describe("defaultProvider", () => {
144174
};
145175

146176
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
177+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
147178
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
148179
(fromProcess() as any).mockImplementation(() => Promise.resolve(creds));
149180

150181
expect(await defaultProvider()()).toEqual(creds);
151182
expect((fromEnv() as any).mock.calls.length).toBe(1);
183+
expect((fromSSO() as any).mock.calls.length).toBe(1);
152184
expect((fromIni() as any).mock.calls.length).toBe(1);
153185
expect((fromProcess() as any).mock.calls.length).toBe(1);
154186
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
@@ -161,12 +193,14 @@ describe("defaultProvider", () => {
161193
secretAccessKey: "bar",
162194
};
163195
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
196+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
164197
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
165198
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
166199
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
167200

168201
expect(await defaultProvider()()).toEqual(creds);
169202
expect((fromEnv() as any).mock.calls.length).toBe(1);
203+
expect((fromSSO() as any).mock.calls.length).toBe(1);
170204
expect((fromIni() as any).mock.calls.length).toBe(1);
171205
expect((fromProcess() as any).mock.calls.length).toBe(1);
172206
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
@@ -180,6 +214,7 @@ describe("defaultProvider", () => {
180214
};
181215

182216
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
217+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
183218
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
184219
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
185220
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
@@ -198,6 +233,7 @@ describe("defaultProvider", () => {
198233
};
199234

200235
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
236+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
201237
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
202238
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
203239
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
@@ -207,6 +243,7 @@ describe("defaultProvider", () => {
207243

208244
expect(await defaultProvider()()).toEqual(creds);
209245
expect((fromEnv() as any).mock.calls.length).toBe(1);
246+
expect((fromSSO() as any).mock.calls.length).toBe(1);
210247
expect((fromIni() as any).mock.calls.length).toBe(1);
211248
expect((fromProcess() as any).mock.calls.length).toBe(1);
212249
expect((fromContainerMetadata() as any).mock.calls.length).toBe(1);
@@ -220,16 +257,41 @@ describe("defaultProvider", () => {
220257
};
221258

222259
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
260+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("Nope!")));
223261
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
224262
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("Nor here!")));
225263
(fromInstanceMetadata() as any).mockImplementation(() => Promise.resolve(creds));
226264

227265
await expect(defaultProvider()()).resolves;
228266
expect((loadSharedConfigFiles as any).mock.calls.length).toBe(1);
229267
expect((fromIni as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
268+
expect((fromSSO as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
230269
expect((fromProcess as any).mock.calls[1][0]).toMatchObject({ loadedConfig: loadSharedConfigFiles() });
231270
});
232271

272+
it("should pass configuration on to the SSO provider", async () => {
273+
const ssoConfig: FromSSOInit = {
274+
profile: "foo",
275+
filepath: "/home/user/.secrets/credentials.ini",
276+
configFilepath: "/home/user/.secrets/credentials.ini",
277+
};
278+
279+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("Keep moving!")));
280+
(fromSSO() as any).mockImplementation(() =>
281+
Promise.resolve({
282+
accessKeyId: "foo",
283+
secretAccessKey: "bar",
284+
})
285+
);
286+
287+
(fromSSO as any).mockClear();
288+
289+
await expect(defaultProvider(ssoConfig)()).resolves;
290+
291+
expect((fromSSO as any).mock.calls.length).toBe(1);
292+
expect((fromSSO as any).mock.calls[0][0]).toEqual({ ...ssoConfig, loadedConfig });
293+
});
294+
233295
it("should pass configuration on to the ini provider", async () => {
234296
const iniConfig: FromIniInit = {
235297
profile: "foo",
@@ -387,60 +449,86 @@ describe("defaultProvider", () => {
387449

388450
// CF https://github.com/boto/botocore/blob/1.8.32/botocore/credentials.py#L104
389451
describe("explicit profiles", () => {
390-
it("should only consult the ini provider if a profile has been specified", async () => {
452+
it("should only consult SSO provider if profile has been set", async () => {
391453
const creds = {
392454
accessKeyId: "foo",
393455
secretAccessKey: "bar",
394456
};
395457

396-
(fromEnv() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
397-
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
398-
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
399-
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
458+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
459+
(fromSSO() as any).mockImplementation(() => Promise.resolve(Promise.resolve(creds)));
460+
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
461+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
462+
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
400463

401464
expect(await defaultProvider({ profile: "foo" })()).toEqual(creds);
402465
expect((fromEnv() as any).mock.calls.length).toBe(0);
403-
expect((fromIni() as any).mock.calls.length).toBe(1);
466+
expect((fromSSO() as any).mock.calls.length).toBe(1);
467+
expect((fromIni() as any).mock.calls.length).toBe(0);
404468
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
405469
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
406470
});
407471

408-
it("should only consult the ini provider if the profile environment variable has been set", async () => {
472+
it("should only consult SSO provider if the profile environment variable has been set", async () => {
409473
const creds = {
410474
accessKeyId: "foo",
411475
secretAccessKey: "bar",
412476
};
413477

414-
(fromEnv() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
415-
(fromIni() as any).mockImplementation(() => Promise.resolve(creds));
416-
(fromProcess() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
417-
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
418-
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
478+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
479+
(fromSSO() as any).mockImplementation(() => Promise.resolve(creds));
480+
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
481+
(fromProcess() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
482+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
483+
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
419484

420485
process.env[ENV_PROFILE] = "foo";
421486
expect(await defaultProvider()()).toEqual(creds);
422487
expect((fromEnv() as any).mock.calls.length).toBe(0);
423-
expect((fromIni() as any).mock.calls.length).toBe(1);
488+
expect((fromSSO() as any).mock.calls.length).toBe(1);
489+
expect((fromIni() as any).mock.calls.length).toBe(0);
424490
expect((fromProcess() as any).mock.calls.length).toBe(0);
425491
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
426492
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
427493
});
428494

495+
it("should consult ini provider if no credentials is not found in SSO provider", async () => {
496+
const creds = {
497+
accessKeyId: "foo",
498+
secretAccessKey: "bar",
499+
};
500+
501+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
502+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
503+
(fromIni() as any).mockImplementation(() => Promise.resolve(Promise.resolve(creds)));
504+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
505+
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
506+
507+
expect(await defaultProvider({ profile: "foo" })()).toEqual(creds);
508+
expect((fromEnv() as any).mock.calls.length).toBe(0);
509+
expect((fromSSO() as any).mock.calls.length).toBe(1);
510+
expect((fromIni() as any).mock.calls.length).toBe(1);
511+
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);
512+
expect((fromInstanceMetadata() as any).mock.calls.length).toBe(0);
513+
});
514+
429515
it("should consult the process provider if no credentials are found in the ini provider", async () => {
430516
const creds = {
431517
accessKeyId: "foo",
432518
secretAccessKey: "bar",
433519
};
434520

435-
(fromEnv() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
436-
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("Nothing here!")));
521+
(fromEnv() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
522+
(fromSSO() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
523+
(fromIni() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
437524
(fromProcess() as any).mockImplementation(() => Promise.resolve(creds));
438-
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
439-
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new Error("PANIC")));
525+
(fromInstanceMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
526+
(fromContainerMetadata() as any).mockImplementation(() => Promise.reject(new ProviderError("PANIC")));
440527

441528
process.env[ENV_PROFILE] = "foo";
442529
expect(await defaultProvider()()).toEqual(creds);
443530
expect((fromEnv() as any).mock.calls.length).toBe(0);
531+
expect((fromSSO() as any).mock.calls.length).toBe(1);
444532
expect((fromIni() as any).mock.calls.length).toBe(1);
445533
expect((fromProcess() as any).mock.calls.length).toBe(1);
446534
expect((fromContainerMetadata() as any).mock.calls.length).toBe(0);

Diff for: packages/credential-provider-node/src/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "@aws-sdk/credential-provider-imds";
99
import { ENV_PROFILE, fromIni, FromIniInit } from "@aws-sdk/credential-provider-ini";
1010
import { fromProcess, FromProcessInit } from "@aws-sdk/credential-provider-process";
11+
import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
1112
import { chain, memoize, ProviderError } from "@aws-sdk/property-provider";
1213
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
1314
import { CredentialProvider } from "@aws-sdk/types";
@@ -33,6 +34,8 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
3334
*
3435
* @see fromEnv The function used to source credentials from
3536
* environment variables
37+
* @see fromSSO The function used to source credentials from
38+
* resolved SSO token cache
3639
* @see fromIni The function used to source credentials from INI
3740
* files
3841
* @see fromProcess The function used to sources credentials from
@@ -42,10 +45,12 @@ export const ENV_IMDS_DISABLED = "AWS_EC2_METADATA_DISABLED";
4245
* @see fromContainerMetadata The function used to source credentials from the
4346
* ECS Container Metadata Service
4447
*/
45-
export const defaultProvider = (init: FromIniInit & RemoteProviderInit & FromProcessInit = {}): CredentialProvider => {
48+
export const defaultProvider = (
49+
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit = {}
50+
): CredentialProvider => {
4651
const options = { profile: process.env[ENV_PROFILE], ...init };
4752
if (!options.loadedConfig) options.loadedConfig = loadSharedConfigFiles(init);
48-
const providers = [fromIni(options), fromProcess(options), remoteProvider(options)];
53+
const providers = [fromSSO(options), fromIni(options), fromProcess(options), remoteProvider(options)];
4954
if (!options.profile) providers.unshift(fromEnv());
5055
const providerChain = chain(...providers);
5156

0 commit comments

Comments
 (0)