-
Notifications
You must be signed in to change notification settings - Fork 544
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: attempt to make credential-provider-sso modular
- Loading branch information
Showing
6 changed files
with
192 additions
and
170 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { SSOClient } from "@aws-sdk/client-sso"; | ||
import { CredentialsProviderError } from "@aws-sdk/property-provider"; | ||
import { CredentialProvider } from "@aws-sdk/types"; | ||
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials"; | ||
|
||
import { isSsoProfile } from "./isSsoProfile"; | ||
import { resolveSSOCredentials } from "./resolveSSOCredentials"; | ||
import { validateSsoProfile } from "./validateSsoProfile"; | ||
|
||
export interface SsoCredentialsParameters { | ||
/** | ||
* The URL to the AWS SSO service. | ||
*/ | ||
ssoStartUrl: string; | ||
|
||
/** | ||
* The ID of the AWS account to use for temporary credentials. | ||
*/ | ||
ssoAccountId: string; | ||
|
||
/** | ||
* The AWS region to use for temporary credentials. | ||
*/ | ||
ssoRegion: string; | ||
|
||
/** | ||
* The name of the AWS role to assume. | ||
*/ | ||
ssoRoleName: string; | ||
} | ||
|
||
export interface FromSSOInit extends SourceProfileInit { | ||
ssoClient?: SSOClient; | ||
} | ||
/** | ||
* Creates a credential provider that will read from a credential_process specified | ||
* in ini files. | ||
*/ | ||
|
||
export const fromSSO = | ||
(init: FromSSOInit & Partial<SsoCredentialsParameters> = {} as any): CredentialProvider => | ||
async () => { | ||
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient } = init; | ||
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName) { | ||
// Load the SSO config from shared AWS config file. | ||
const profiles = await parseKnownFiles(init); | ||
const profileName = getMasterProfileName(init); | ||
const profile = profiles[profileName]; | ||
if (!isSsoProfile(profile)) { | ||
throw new CredentialsProviderError(`Profile ${profileName} is not configured with SSO credentials.`); | ||
} | ||
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(profile); | ||
return resolveSSOCredentials({ | ||
ssoStartUrl: sso_start_url, | ||
ssoAccountId: sso_account_id, | ||
ssoRegion: sso_region, | ||
ssoRoleName: sso_role_name, | ||
ssoClient: ssoClient, | ||
}); | ||
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) { | ||
throw new CredentialsProviderError( | ||
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' + | ||
' "ssoAccountId", "ssoRegion", "ssoRoleName"' | ||
); | ||
} else { | ||
return resolveSSOCredentials({ ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,170 +1,4 @@ | ||
import { GetRoleCredentialsCommand, GetRoleCredentialsCommandOutput, SSOClient } from "@aws-sdk/client-sso"; | ||
import { CredentialsProviderError } from "@aws-sdk/property-provider"; | ||
import { getHomeDir, Profile } from "@aws-sdk/shared-ini-file-loader"; | ||
import { CredentialProvider, Credentials } from "@aws-sdk/types"; | ||
import { getMasterProfileName, parseKnownFiles, SourceProfileInit } from "@aws-sdk/util-credentials"; | ||
import { createHash } from "crypto"; | ||
import { readFileSync } from "fs"; | ||
import { join } from "path"; | ||
|
||
/** | ||
* The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token. | ||
* This is needed because server side may have invalidated the token before the defined expiration date. | ||
* | ||
* @internal | ||
*/ | ||
export const EXPIRE_WINDOW_MS = 15 * 60 * 1000; | ||
|
||
const SHOULD_FAIL_CREDENTIAL_CHAIN = false; | ||
|
||
/** | ||
* Cached SSO token retrieved from SSO login flow. | ||
*/ | ||
interface SSOToken { | ||
// A base64 encoded string returned by the sso-oidc service. | ||
accessToken: string; | ||
// RFC3339 format timestamp | ||
expiresAt: string; | ||
region?: string; | ||
startUrl?: string; | ||
} | ||
|
||
export interface SsoCredentialsParameters { | ||
/** | ||
* The URL to the AWS SSO service. | ||
*/ | ||
ssoStartUrl: string; | ||
|
||
/** | ||
* The ID of the AWS account to use for temporary credentials. | ||
*/ | ||
ssoAccountId: string; | ||
|
||
/** | ||
* The AWS region to use for temporary credentials. | ||
*/ | ||
ssoRegion: string; | ||
|
||
/** | ||
* The name of the AWS role to assume. | ||
*/ | ||
ssoRoleName: string; | ||
} | ||
export interface FromSSOInit extends SourceProfileInit { | ||
ssoClient?: SSOClient; | ||
} | ||
|
||
/** | ||
* Creates a credential provider that will read from a credential_process specified | ||
* in ini files. | ||
*/ | ||
export const fromSSO = | ||
(init: FromSSOInit & Partial<SsoCredentialsParameters> = {} as any): CredentialProvider => | ||
async () => { | ||
const { ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient } = init; | ||
if (!ssoStartUrl && !ssoAccountId && !ssoRegion && !ssoRoleName) { | ||
// Load the SSO config from shared AWS config file. | ||
const profiles = await parseKnownFiles(init); | ||
const profileName = getMasterProfileName(init); | ||
const profile = profiles[profileName]; | ||
if (!isSsoProfile(profile)) { | ||
throw new CredentialsProviderError(`Profile ${profileName} is not configured with SSO credentials.`); | ||
} | ||
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = validateSsoProfile(profile); | ||
return resolveSSOCredentials({ | ||
ssoStartUrl: sso_start_url, | ||
ssoAccountId: sso_account_id, | ||
ssoRegion: sso_region, | ||
ssoRoleName: sso_role_name, | ||
ssoClient: ssoClient, | ||
}); | ||
} else if (!ssoStartUrl || !ssoAccountId || !ssoRegion || !ssoRoleName) { | ||
throw new CredentialsProviderError( | ||
'Incomplete configuration. The fromSSO() argument hash must include "ssoStartUrl",' + | ||
' "ssoAccountId", "ssoRegion", "ssoRoleName"' | ||
); | ||
} else { | ||
return resolveSSOCredentials({ ssoStartUrl, ssoAccountId, ssoRegion, ssoRoleName, ssoClient }); | ||
} | ||
}; | ||
|
||
const resolveSSOCredentials = async ({ | ||
ssoStartUrl, | ||
ssoAccountId, | ||
ssoRegion, | ||
ssoRoleName, | ||
ssoClient, | ||
}: FromSSOInit & SsoCredentialsParameters): Promise<Credentials> => { | ||
const hasher = createHash("sha1"); | ||
const cacheName = hasher.update(ssoStartUrl).digest("hex"); | ||
const tokenFile = join(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`); | ||
let token: SSOToken; | ||
try { | ||
token = JSON.parse(readFileSync(tokenFile, { encoding: "utf-8" })); | ||
if (new Date(token.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) { | ||
throw new Error("SSO token is expired."); | ||
} | ||
} catch (e) { | ||
throw new CredentialsProviderError( | ||
`The SSO session associated with this profile has expired or is otherwise invalid. To refresh this SSO session ` + | ||
`run aws sso login with the corresponding profile.`, | ||
SHOULD_FAIL_CREDENTIAL_CHAIN | ||
); | ||
} | ||
const { accessToken } = token; | ||
const sso = ssoClient || new SSOClient({ region: ssoRegion }); | ||
let ssoResp: GetRoleCredentialsCommandOutput; | ||
try { | ||
ssoResp = await sso.send( | ||
new GetRoleCredentialsCommand({ | ||
accountId: ssoAccountId, | ||
roleName: ssoRoleName, | ||
accessToken, | ||
}) | ||
); | ||
} catch (e) { | ||
throw CredentialsProviderError.from(e, SHOULD_FAIL_CREDENTIAL_CHAIN); | ||
} | ||
const { roleCredentials: { accessKeyId, secretAccessKey, sessionToken, expiration } = {} } = ssoResp; | ||
if (!accessKeyId || !secretAccessKey || !sessionToken || !expiration) { | ||
throw new CredentialsProviderError("SSO returns an invalid temporary credential.", SHOULD_FAIL_CREDENTIAL_CHAIN); | ||
} | ||
return { accessKeyId, secretAccessKey, sessionToken, expiration: new Date(expiration) }; | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export interface SsoProfile extends Profile { | ||
sso_start_url: string; | ||
sso_account_id: string; | ||
sso_region: string; | ||
sso_role_name: string; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const validateSsoProfile = (profile: Partial<SsoProfile>): SsoProfile => { | ||
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = profile; | ||
if (!sso_start_url || !sso_account_id || !sso_region || !sso_role_name) { | ||
throw new CredentialsProviderError( | ||
`Profile is configured with invalid SSO credentials. Required parameters "sso_account_id", "sso_region", ` + | ||
`"sso_role_name", "sso_start_url". Got ${Object.keys(profile).join( | ||
", " | ||
)}\nReference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html`, | ||
SHOULD_FAIL_CREDENTIAL_CHAIN | ||
); | ||
} | ||
return profile as SsoProfile; | ||
}; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const isSsoProfile = (arg: Profile): arg is Partial<SsoProfile> => | ||
arg && | ||
(typeof arg.sso_start_url === "string" || | ||
typeof arg.sso_account_id === "string" || | ||
typeof arg.sso_region === "string" || | ||
typeof arg.sso_role_name === "string"); | ||
export * from "./fromSSO"; | ||
export * from "./isSsoProfile"; | ||
export * from "./types"; | ||
export * from "./validateSsoProfile"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Profile } from "@aws-sdk/shared-ini-file-loader"; | ||
|
||
import { SsoProfile } from "./types"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const isSsoProfile = (arg: Profile): arg is Partial<SsoProfile> => | ||
arg && | ||
(typeof arg.sso_start_url === "string" || | ||
typeof arg.sso_account_id === "string" || | ||
typeof arg.sso_region === "string" || | ||
typeof arg.sso_role_name === "string"); |
64 changes: 64 additions & 0 deletions
64
packages/credential-provider-sso/src/resolveSSOCredentials.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { GetRoleCredentialsCommand, GetRoleCredentialsCommandOutput, SSOClient } from "@aws-sdk/client-sso"; | ||
import { CredentialsProviderError } from "@aws-sdk/property-provider"; | ||
import { getHomeDir } from "@aws-sdk/shared-ini-file-loader"; | ||
import { Credentials } from "@aws-sdk/types"; | ||
import { createHash } from "crypto"; | ||
import { readFileSync } from "fs"; | ||
import { join } from "path"; | ||
|
||
import { FromSSOInit, SsoCredentialsParameters } from "./fromSSO"; | ||
import { SSOToken } from "./types"; | ||
|
||
/** | ||
* The time window (15 mins) that SDK will treat the SSO token expires in before the defined expiration date in token. | ||
* This is needed because server side may have invalidated the token before the defined expiration date. | ||
* | ||
* @internal | ||
*/ | ||
const EXPIRE_WINDOW_MS = 15 * 60 * 1000; | ||
|
||
const SHOULD_FAIL_CREDENTIAL_CHAIN = false; | ||
|
||
export const resolveSSOCredentials = async ({ | ||
ssoStartUrl, | ||
ssoAccountId, | ||
ssoRegion, | ||
ssoRoleName, | ||
ssoClient, | ||
}: FromSSOInit & SsoCredentialsParameters): Promise<Credentials> => { | ||
const hasher = createHash("sha1"); | ||
const cacheName = hasher.update(ssoStartUrl).digest("hex"); | ||
const tokenFile = join(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`); | ||
let token: SSOToken; | ||
try { | ||
token = JSON.parse(readFileSync(tokenFile, { encoding: "utf-8" })); | ||
if (new Date(token.expiresAt).getTime() - Date.now() <= EXPIRE_WINDOW_MS) { | ||
throw new Error("SSO token is expired."); | ||
} | ||
} catch (e) { | ||
throw new CredentialsProviderError( | ||
`The SSO session associated with this profile has expired or is otherwise invalid. To refresh this SSO session ` + | ||
`run aws sso login with the corresponding profile.`, | ||
SHOULD_FAIL_CREDENTIAL_CHAIN | ||
); | ||
} | ||
const { accessToken } = token; | ||
const sso = ssoClient || new SSOClient({ region: ssoRegion }); | ||
let ssoResp: GetRoleCredentialsCommandOutput; | ||
try { | ||
ssoResp = await sso.send( | ||
new GetRoleCredentialsCommand({ | ||
accountId: ssoAccountId, | ||
roleName: ssoRoleName, | ||
accessToken, | ||
}) | ||
); | ||
} catch (e) { | ||
throw CredentialsProviderError.from(e, SHOULD_FAIL_CREDENTIAL_CHAIN); | ||
} | ||
const { roleCredentials: { accessKeyId, secretAccessKey, sessionToken, expiration } = {} } = ssoResp; | ||
if (!accessKeyId || !secretAccessKey || !sessionToken || !expiration) { | ||
throw new CredentialsProviderError("SSO returns an invalid temporary credential.", SHOULD_FAIL_CREDENTIAL_CHAIN); | ||
} | ||
return { accessKeyId, secretAccessKey, sessionToken, expiration: new Date(expiration) }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Profile } from "@aws-sdk/shared-ini-file-loader"; | ||
|
||
/** | ||
* Cached SSO token retrieved from SSO login flow. | ||
*/ | ||
export interface SSOToken { | ||
// A base64 encoded string returned by the sso-oidc service. | ||
accessToken: string; | ||
// RFC3339 format timestamp | ||
expiresAt: string; | ||
region?: string; | ||
startUrl?: string; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export interface SsoProfile extends Profile { | ||
sso_start_url: string; | ||
sso_account_id: string; | ||
sso_region: string; | ||
sso_role_name: string; | ||
} |
20 changes: 20 additions & 0 deletions
20
packages/credential-provider-sso/src/validateSsoProfile.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { CredentialsProviderError } from "@aws-sdk/property-provider"; | ||
|
||
import { SsoProfile } from "./types"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export const validateSsoProfile = (profile: Partial<SsoProfile>): SsoProfile => { | ||
const { sso_start_url, sso_account_id, sso_region, sso_role_name } = profile; | ||
if (!sso_start_url || !sso_account_id || !sso_region || !sso_role_name) { | ||
throw new CredentialsProviderError( | ||
`Profile is configured with invalid SSO credentials. Required parameters "sso_account_id", "sso_region", ` + | ||
`"sso_role_name", "sso_start_url". Got ${Object.keys(profile).join( | ||
", " | ||
)}\nReference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html`, | ||
false | ||
); | ||
} | ||
return profile as SsoProfile; | ||
}; |