Skip to content

Commit c0467f3

Browse files
committed
chore: attempt to make credential-provider-sso modular
1 parent e7d0ff9 commit c0467f3

File tree

6 files changed

+192
-170
lines changed

6 files changed

+192
-170
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { SSOClient } from "@aws-sdk/client-sso";
2+
import { CredentialsProviderError } from "@aws-sdk/property-provider";
3+
import { CredentialProvider } from "@aws-sdk/types";
4+
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";
5+
6+
import { isSsoProfile } from "./isSsoProfile";
7+
import { resolveSSOCredentials } from "./resolveSSOCredentials";
8+
import { validateSsoProfile } from "./validateSsoProfile";
9+
10+
export interface SsoCredentialsParameters {
11+
/**
12+
* The URL to the AWS SSO service.
13+
*/
14+
ssoStartUrl: string;
15+
16+
/**
17+
* The ID of the AWS account to use for temporary credentials.
18+
*/
19+
ssoAccountId: string;
20+
21+
/**
22+
* The AWS region to use for temporary credentials.
23+
*/
24+
ssoRegion: string;
25+
26+
/**
27+
* The name of the AWS role to assume.
28+
*/
29+
ssoRoleName: string;
30+
}
31+
32+
export interface FromSSOInit extends SourceProfileInit {
33+
ssoClient?: SSOClient;
34+
}
35+
/**
36+
* Creates a credential provider that will read from a credential_process specified
37+
* in ini files.
38+
*/
39+
40+
export const fromSSO =
41+
(init: FromSSOInit & Partial<SsoCredentialsParameters> = {} as any): CredentialProvider =>
42+
async () => {
43+
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient } = init;
44+
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName) {
45+
// Load the SSO config from shared AWS config file.
46+
const profiles = await parseKnownFiles(init);
47+
const profileName = getMasterProfileName(init);
48+
const profile = profiles[profileName];
49+
if (!isSsoProfile(profile)) {
50+
throw new CredentialsProviderError(`Profile ${profileName} is not configured with SSO credentials.`);
51+
}
52+
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(profile);
53+
return resolveSSOCredentials({
54+
ssoStartUrl: sso_start_url,
55+
ssoAccountId: sso_account_id,
56+
ssoRegion: sso_region,
57+
ssoRoleName: sso_role_name,
58+
ssoClient: ssoClient,
59+
});
60+
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) {
61+
throw new CredentialsProviderError(
62+
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' +
63+
' "ssoAccountId", "ssoRegion", "ssoRoleName"'
64+
);
65+
} else {
66+
return resolveSSOCredentials({ ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient });
67+
}
68+
};
Lines changed: 4 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,4 @@
1-
import { GetRoleCredentialsCommand, GetRoleCredentialsCommandOutput, SSOClient } from "@aws-sdk/client-sso";
2-
import { CredentialsProviderError } from "@aws-sdk/property-provider";
3-
import { getHomeDir, Profile } from "@aws-sdk/shared-ini-file-loader";
4-
import { CredentialProvider, Credentials } from "@aws-sdk/types";
5-
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials";
6-
import { createHash } from "crypto";
7-
import { readFileSync } from "fs";
8-
import { join } from "path";
9-
10-
/**
11-
* The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
12-
* This is needed because server side may have invalidated the token before the defined expiration date.
13-
*
14-
* @internal
15-
*/
16-
export const EXPIRE_WINDOW_MS = 15 * 60 * 1000;
17-
18-
const SHOULD_FAIL_CREDENTIAL_CHAIN = false;
19-
20-
/**
21-
* Cached SSO token retrieved from SSO login flow.
22-
*/
23-
interface SSOToken {
24-
// A base64 encoded string returned by the sso-oidc service.
25-
accessToken: string;
26-
// RFC3339 format timestamp
27-
expiresAt: string;
28-
region?: string;
29-
startUrl?: string;
30-
}
31-
32-
export interface SsoCredentialsParameters {
33-
/**
34-
* The URL to the AWS SSO service.
35-
*/
36-
ssoStartUrl: string;
37-
38-
/**
39-
* The ID of the AWS account to use for temporary credentials.
40-
*/
41-
ssoAccountId: string;
42-
43-
/**
44-
* The AWS region to use for temporary credentials.
45-
*/
46-
ssoRegion: string;
47-
48-
/**
49-
* The name of the AWS role to assume.
50-
*/
51-
ssoRoleName: string;
52-
}
53-
export interface FromSSOInit extends SourceProfileInit {
54-
ssoClient?: SSOClient;
55-
}
56-
57-
/**
58-
* Creates a credential provider that will read from a credential_process specified
59-
* in ini files.
60-
*/
61-
export const fromSSO =
62-
(init: FromSSOInit & Partial<SsoCredentialsParameters> = {} as any): CredentialProvider =>
63-
async () => {
64-
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient } = init;
65-
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName) {
66-
// Load the SSO config from shared AWS config file.
67-
const profiles = await parseKnownFiles(init);
68-
const profileName = getMasterProfileName(init);
69-
const profile = profiles[profileName];
70-
if (!isSsoProfile(profile)) {
71-
throw new CredentialsProviderError(`Profile ${profileName} is not configured with SSO credentials.`);
72-
}
73-
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(profile);
74-
return resolveSSOCredentials({
75-
ssoStartUrl: sso_start_url,
76-
ssoAccountId: sso_account_id,
77-
ssoRegion: sso_region,
78-
ssoRoleName: sso_role_name,
79-
ssoClient: ssoClient,
80-
});
81-
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) {
82-
throw new CredentialsProviderError(
83-
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' +
84-
' "ssoAccountId", "ssoRegion", "ssoRoleName"'
85-
);
86-
} else {
87-
return resolveSSOCredentials({ ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient });
88-
}
89-
};
90-
91-
const resolveSSOCredentials = async ({
92-
ssoStartUrl,
93-
ssoAccountId,
94-
ssoRegion,
95-
ssoRoleName,
96-
ssoClient,
97-
}: FromSSOInit & SsoCredentialsParameters): Promise<Credentials> => {
98-
const hasher = createHash("sha1");
99-
const cacheName = hasher.update(ssoStartUrl).digest("hex");
100-
const tokenFile = join(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`);
101-
let token: SSOToken;
102-
try {
103-
token = JSON.parse(readFileSync(tokenFile, { encoding: "utf-8" }));
104-
if (new Date(token.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) {
105-
throw new Error("SSO token is expired.");
106-
}
107-
} catch (e) {
108-
throw new CredentialsProviderError(
109-
`The SSO session associated with this profile has expired or is otherwise invalid. To refresh this SSO session ` +
110-
`run aws sso login with the corresponding profile.`,
111-
SHOULD_FAIL_CREDENTIAL_CHAIN
112-
);
113-
}
114-
const { accessToken } = token;
115-
const sso = ssoClient || new SSOClient({ region: ssoRegion });
116-
let ssoResp: GetRoleCredentialsCommandOutput;
117-
try {
118-
ssoResp = await sso.send(
119-
new GetRoleCredentialsCommand({
120-
accountId: ssoAccountId,
121-
roleName: ssoRoleName,
122-
accessToken,
123-
})
124-
);
125-
} catch (e) {
126-
throw CredentialsProviderError.from(e, SHOULD_FAIL_CREDENTIAL_CHAIN);
127-
}
128-
const { roleCredentials: { accessKeyId, secretAccessKey, sessionToken, expiration } = {} } = ssoResp;
129-
if (!accessKeyId || !secretAccessKey || !sessionToken || !expiration) {
130-
throw new CredentialsProviderError("SSO returns an invalid temporary credential.", SHOULD_FAIL_CREDENTIAL_CHAIN);
131-
}
132-
return { accessKeyId, secretAccessKey, sessionToken, expiration: new Date(expiration) };
133-
};
134-
135-
/**
136-
* @internal
137-
*/
138-
export interface SsoProfile extends Profile {
139-
sso_start_url: string;
140-
sso_account_id: string;
141-
sso_region: string;
142-
sso_role_name: string;
143-
}
144-
145-
/**
146-
* @internal
147-
*/
148-
export const validateSsoProfile = (profile: Partial<SsoProfile>): SsoProfile => {
149-
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = profile;
150-
if (!sso_start_url || !sso_account_id || !sso_region || !sso_role_name) {
151-
throw new CredentialsProviderError(
152-
`Profile is configured with invalid SSO credentials. Required parameters "sso_account_id", "sso_region", ` +
153-
`"sso_role_name", "sso_start_url". Got ${Object.keys(profile).join(
154-
", "
155-
)}\nReference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html`,
156-
SHOULD_FAIL_CREDENTIAL_CHAIN
157-
);
158-
}
159-
return profile as SsoProfile;
160-
};
161-
162-
/**
163-
* @internal
164-
*/
165-
export const isSsoProfile = (arg: Profile): arg is Partial<SsoProfile> =>
166-
arg &&
167-
(typeof arg.sso_start_url === "string" ||
168-
typeof arg.sso_account_id === "string" ||
169-
typeof arg.sso_region === "string" ||
170-
typeof arg.sso_role_name === "string");
1+
export * from "./fromSSO";
2+
export * from "./isSsoProfile";
3+
export * from "./types";
4+
export * from "./validateSsoProfile";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Profile } from "@aws-sdk/shared-ini-file-loader";
2+
3+
import { SsoProfile } from "./types";
4+
5+
/**
6+
* @internal
7+
*/
8+
export const isSsoProfile = (arg: Profile): arg is Partial<SsoProfile> =>
9+
arg &&
10+
(typeof arg.sso_start_url === "string" ||
11+
typeof arg.sso_account_id === "string" ||
12+
typeof arg.sso_region === "string" ||
13+
typeof arg.sso_role_name === "string");
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { GetRoleCredentialsCommand, GetRoleCredentialsCommandOutput, SSOClient } from "@aws-sdk/client-sso";
2+
import { CredentialsProviderError } from "@aws-sdk/property-provider";
3+
import { getHomeDir } from "@aws-sdk/shared-ini-file-loader";
4+
import { Credentials } from "@aws-sdk/types";
5+
import { createHash } from "crypto";
6+
import { readFileSync } from "fs";
7+
import { join } from "path";
8+
9+
import { FromSSOInit, SsoCredentialsParameters } from "./fromSSO";
10+
import { SSOToken } from "./types";
11+
12+
/**
13+
* The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token.
14+
* This is needed because server side may have invalidated the token before the defined expiration date.
15+
*
16+
* @internal
17+
*/
18+
const EXPIRE_WINDOW_MS = 15 * 60 * 1000;
19+
20+
const SHOULD_FAIL_CREDENTIAL_CHAIN = false;
21+
22+
export const resolveSSOCredentials = async ({
23+
ssoStartUrl,
24+
ssoAccountId,
25+
ssoRegion,
26+
ssoRoleName,
27+
ssoClient,
28+
}: FromSSOInit & SsoCredentialsParameters): Promise<Credentials> => {
29+
const hasher = createHash("sha1");
30+
const cacheName = hasher.update(ssoStartUrl).digest("hex");
31+
const tokenFile = join(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`);
32+
let token: SSOToken;
33+
try {
34+
token = JSON.parse(readFileSync(tokenFile, { encoding: "utf-8" }));
35+
if (new Date(token.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) {
36+
throw new Error("SSO token is expired.");
37+
}
38+
} catch (e) {
39+
throw new CredentialsProviderError(
40+
`The SSO session associated with this profile has expired or is otherwise invalid. To refresh this SSO session ` +
41+
`run aws sso login with the corresponding profile.`,
42+
SHOULD_FAIL_CREDENTIAL_CHAIN
43+
);
44+
}
45+
const { accessToken } = token;
46+
const sso = ssoClient || new SSOClient({ region: ssoRegion });
47+
let ssoResp: GetRoleCredentialsCommandOutput;
48+
try {
49+
ssoResp = await sso.send(
50+
new GetRoleCredentialsCommand({
51+
accountId: ssoAccountId,
52+
roleName: ssoRoleName,
53+
accessToken,
54+
})
55+
);
56+
} catch (e) {
57+
throw CredentialsProviderError.from(e, SHOULD_FAIL_CREDENTIAL_CHAIN);
58+
}
59+
const { roleCredentials: { accessKeyId, secretAccessKey, sessionToken, expiration } = {} } = ssoResp;
60+
if (!accessKeyId || !secretAccessKey || !sessionToken || !expiration) {
61+
throw new CredentialsProviderError("SSO returns an invalid temporary credential.", SHOULD_FAIL_CREDENTIAL_CHAIN);
62+
}
63+
return { accessKeyId, secretAccessKey, sessionToken, expiration: new Date(expiration) };
64+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Profile } from "@aws-sdk/shared-ini-file-loader";
2+
3+
/**
4+
* Cached SSO token retrieved from SSO login flow.
5+
*/
6+
export interface SSOToken {
7+
// A base64 encoded string returned by the sso-oidc service.
8+
accessToken: string;
9+
// RFC3339 format timestamp
10+
expiresAt: string;
11+
region?: string;
12+
startUrl?: string;
13+
}
14+
15+
/**
16+
* @internal
17+
*/
18+
export interface SsoProfile extends Profile {
19+
sso_start_url: string;
20+
sso_account_id: string;
21+
sso_region: string;
22+
sso_role_name: string;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { CredentialsProviderError } from "@aws-sdk/property-provider";
2+
3+
import { SsoProfile } from "./types";
4+
5+
/**
6+
* @internal
7+
*/
8+
export const validateSsoProfile = (profile: Partial<SsoProfile>): SsoProfile => {
9+
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = profile;
10+
if (!sso_start_url || !sso_account_id || !sso_region || !sso_role_name) {
11+
throw new CredentialsProviderError(
12+
`Profile is configured with invalid SSO credentials. Required parameters "sso_account_id", "sso_region", ` +
13+
`"sso_role_name", "sso_start_url". Got ${Object.keys(profile).join(
14+
", "
15+
)}\nReference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html`,
16+
false
17+
);
18+
}
19+
return profile as SsoProfile;
20+
};

0 commit comments

Comments
 (0)