|
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"; |
0 commit comments