Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deps(auth): remove dependence on deprecated and outdated @aws-sdk/* packages. #6474

Merged
merged 17 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/faq-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ Issue [aws-toolkit-vscode#3667](https://github.com/aws/aws-toolkit-vscode/issues
2. Attempt to sign in again with AWS Builder ID
3. If sign is is successful you can remove the old folder: `rm -rf ~/.aws/sso-OLD`
1. Or revert the change: `mv ~/.aws/sso-OLD ~/.aws/sso`

### AWS Shared Credentials File

When authenticating with IAM credentials, the profile name, access key, and secret key will be stored on disk at a default location of `~/.aws/credentials` on Linux and MacOS, and `%USERPROFILE%\.aws\credentials` on Windows machines. The toolkit also supports editting this file manually, with the format specified [here](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds). The credentials files also supports [role assumption](https://docs.aws.amazon.com/sdkref/latest/guide/access-assume-role.html) and [MFA](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html). Note that this credentials file is shared between all local AWS development tools. For more information, see the full documentation [here](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html).
30,479 changes: 17,726 additions & 12,753 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@
"@aws-sdk/client-lambda": "^3.637.0",
"@aws-sdk/client-sso": "^3.342.0",
"@aws-sdk/client-sso-oidc": "^3.574.0",
"@aws-sdk/credential-provider-ini": "3.46.0",
"@aws-sdk/credential-provider-env": "3.696.0",
"@aws-sdk/credential-provider-process": "3.37.0",
"@aws-sdk/credential-provider-sso": "^3.345.0",
"@aws-sdk/property-provider": "3.46.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Credentials } from '@aws-sdk/types'
import { fromInstanceMetadata } from '@aws-sdk/credential-provider-imds'
import { fromInstanceMetadata } from '@smithy/credential-provider-imds'
import { DefaultEc2MetadataClient } from '../../shared/clients/ec2MetadataClient'
import { Ec2MetadataClient } from '../../shared/clients/ec2MetadataClient'
import { getLogger } from '../../shared/logger/logger'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Credentials, CredentialProvider } from '@aws-sdk/types'
import { fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
import { fromContainerMetadata } from '@smithy/credential-provider-imds'
import { EnvironmentVariables } from '../../shared/environmentVariables'
import { CredentialType } from '../../shared/telemetry/telemetry.gen'
import { getStringHash } from '../../shared/utilities/textUtilities'
Expand Down
73 changes: 48 additions & 25 deletions packages/core/src/auth/providers/sharedCredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
*/

import * as AWS from '@aws-sdk/types'
import { AssumeRoleParams, fromIni } from '@aws-sdk/credential-provider-ini'
import { fromProcess } from '@aws-sdk/credential-provider-process'
import { ParsedIniData, SharedConfigFiles } from '@smithy/shared-ini-file-loader'
import { ParsedIniData } from '@smithy/types'
import { chain } from '@aws-sdk/property-provider'
import { fromInstanceMetadata, fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
import { fromInstanceMetadata, fromContainerMetadata } from '@smithy/credential-provider-imds'
import { fromEnv } from '@aws-sdk/credential-provider-env'
import { getLogger } from '../../shared/logger/logger'
import { getStringHash } from '../../shared/utilities/textUtilities'
Expand All @@ -29,9 +28,10 @@ import {
Profile,
Section,
} from '../credentials/sharedCredentials'
import { SectionName, SharedCredentialsKeys } from '../credentials/types'
import { CredentialsData, SectionName, SharedCredentialsKeys } from '../credentials/types'
import { SsoProfile, hasScopes, scopesSsoAccountAccess } from '../connection'
import { builderIdStartUrl } from '../sso/constants'
import { ToolkitError } from '../../shared/errors'

const credentialSources = {
ECS_CONTAINER: 'EcsContainer',
Expand Down Expand Up @@ -378,18 +378,6 @@ export class SharedCredentialsProvider implements CredentialsProvider {
}

private makeSharedIniFileCredentialsProvider(loadedCreds?: ParsedIniData): AWS.CredentialProvider {
const assumeRole = async (credentials: AWS.Credentials, params: AssumeRoleParams) => {
const region = this.getDefaultRegion() ?? 'us-east-1'
const stsClient = new DefaultStsClient(region, credentials)
const response = await stsClient.assumeRole(params)
return {
accessKeyId: response.Credentials!.AccessKeyId!,
secretAccessKey: response.Credentials!.SecretAccessKey!,
sessionToken: response.Credentials?.SessionToken,
expiration: response.Credentials?.Expiration,
}
}

// Our credentials logic merges profiles from the credentials and config files but SDK v3 does not
// This can cause odd behavior where the Toolkit can switch to a profile but not authenticate with it
// So the workaround is to do give the SDK the merged profiles directly
Expand All @@ -399,15 +387,50 @@ export class SharedCredentialsProvider implements CredentialsProvider {
(k) => this.getProfile(k)
)

return fromIni({
profile: this.profileName,
mfaCodeProvider: async (mfaSerial) => await getMfaTokenFromUser(mfaSerial, this.profileName),
roleAssumer: assumeRole,
loadedConfig: Promise.resolve({
credentialsFile: loadedCreds ?? profiles,
configFile: {},
} as SharedConfigFiles),
})
return async () => {
const iniData = loadedCreds ?? profiles
const profile: CredentialsData = iniData[this.profileName]
if (!profile) {
throw new ToolkitError(`auth: Profile ${this.profileName} not found`)
}
// No role to assume, return static credentials.
if (!profile.role_arn) {
return {
accessKeyId: profile.aws_access_key_id!,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I know very little about auth but had a couple questions). It looks like you're asserting that profile.aws_access_key_id is not undefined. Is that for sure? Or just to satisfy the accessKeyId type? I'm wonder if it would make sense to warn if access key id/secret access key aren't defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that these fields are required. Authenticating from a config file can be done by directly using the access key and secret key, or using it to assume an IAM role for authentication. However, either way the access key and secret key is required in the current profile or the source profile.

To enforce this, we validate the config file here:

public validate(): string | undefined {
if (hasProps(this.profile, SharedCredentialsKeys.CREDENTIAL_SOURCE)) {
return this.validateSourcedCredentials()
} else if (hasProps(this.profile, SharedCredentialsKeys.ROLE_ARN)) {
return this.validateSourceProfileChain()
} else if (hasProps(this.profile, SharedCredentialsKeys.CREDENTIAL_PROCESS)) {
// No validation. Don't check anything else.
return undefined
} else if (
hasProps(this.profile, SharedCredentialsKeys.AWS_ACCESS_KEY_ID) ||
hasProps(this.profile, SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY) ||
hasProps(this.profile, SharedCredentialsKeys.AWS_SESSION_TOKEN)
) {
return validateProfile(
this.profile,
SharedCredentialsKeys.AWS_ACCESS_KEY_ID,
SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY
)
} else if (isSsoProfile(this.profile)) {
return undefined
} else {
return 'not supported by the Toolkit'
}
}
and then only call this code(makeSharedIniFileCredentialsProvider) when we have the right conditions:
private makeCredentialsProvider(loadedCreds?: ParsedIniData): AWS.CredentialProvider {
const logger = getLogger()
if (hasProps(this.profile, SharedCredentialsKeys.CREDENTIAL_SOURCE)) {
logger.verbose(
`Profile ${this.profileName} contains ${SharedCredentialsKeys.CREDENTIAL_SOURCE} - treating as Environment Credentials`
)
return this.makeSourcedCredentialsProvider()
}
if (hasProps(this.profile, SharedCredentialsKeys.ROLE_ARN)) {
logger.verbose(
`Profile ${this.profileName} contains ${SharedCredentialsKeys.ROLE_ARN} - treating as regular Shared Credentials`
)
return this.makeSharedIniFileCredentialsProvider(loadedCreds)
}
if (hasProps(this.profile, SharedCredentialsKeys.CREDENTIAL_PROCESS)) {
logger.verbose(
`Profile ${this.profileName} contains ${SharedCredentialsKeys.CREDENTIAL_PROCESS} - treating as Process Credentials`
)
return fromProcess({ profile: this.profileName })
}
if (hasProps(this.profile, SharedCredentialsKeys.AWS_SESSION_TOKEN)) {
logger.verbose(
`Profile ${this.profileName} contains ${SharedCredentialsKeys.AWS_SESSION_TOKEN} - treating as regular Shared Credentials`
)
return this.makeSharedIniFileCredentialsProvider(loadedCreds)
}
if (hasProps(this.profile, SharedCredentialsKeys.AWS_ACCESS_KEY_ID)) {
logger.verbose(
`Profile ${this.profileName} contains ${SharedCredentialsKeys.AWS_ACCESS_KEY_ID} - treating as regular Shared Credentials`
)
return this.makeSharedIniFileCredentialsProvider(loadedCreds)
}
if (isSsoProfile(this.profile)) {
logger.verbose(`Profile ${this.profileName} is an SSO profile - treating as SSO Credentials`)
return this.makeSsoCredentaislProvider()
}
logger.error(`Profile ${this.profileName} did not contain any supported properties`)
throw new Error(`Shared Credentials profile ${this.profileName} is not supported`)
}
.

Its a little hard to understand because this code lives in two places, but I don't believes its possible this function is called without the necessary keys existing.

secretAccessKey: profile.aws_secret_access_key!,
sessionToken: profile.aws_session_token,
}
}
if (!profile.source_profile || !iniData[profile.source_profile]) {
throw new ToolkitError(
`auth: Profile ${this.profileName} is missing source_profile for role assumption`
)
}
// Use source profile to assume IAM role based on role ARN provided.
const sourceProfile = iniData[profile.source_profile!]
const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', {
accessKeyId: sourceProfile.aws_access_key_id!,
secretAccessKey: sourceProfile.aws_secret_access_key!,
})
// Prompt for MFA Token if needed.
const assumeRoleReq = {
RoleArn: profile.role_arn,
RoleSessionName: 'AssumeRoleSession',
...(profile.mfa_serial
? {
SerialNumber: profile.mfa_serial,
TokenCode: await getMfaTokenFromUser(profile.mfa_serial, this.profileName),
}
: {}),
}
const assumeRoleRsp = await stsClient.assumeRole(assumeRoleReq)
return {
accessKeyId: assumeRoleRsp.Credentials!.AccessKeyId!,
secretAccessKey: assumeRoleRsp.Credentials!.SecretAccessKey!,
sessionToken: assumeRoleRsp.Credentials?.SessionToken,
expiration: assumeRoleRsp.Credentials?.Expiration,
}
}
}

private makeSourcedCredentialsProvider(): AWS.CredentialProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { SsoClient } from '../../../auth/sso/clients'
import { stub } from '../../utilities/stubber'
import { SsoAccessTokenProvider } from '../../../auth/sso/ssoAccessTokenProvider'
import { createTestSections } from '../testUtil'
import { DefaultStsClient } from '../../../shared/clients/stsClient'
import { oneDay } from '../../../shared/datetime'
import { getTestWindow } from '../../shared/vscode/window'

const missingPropertiesFragment = 'missing properties'

Expand Down Expand Up @@ -450,6 +453,76 @@ describe('SharedCredentialsProvider', async function () {
})
})
})

describe('makeSharedIniFileCredentialsProvider', function () {
let defaultSection: string

before(function () {
defaultSection = `[profile default]
aws_access_key_id = x
aws_secret_access_key = y`
})

beforeEach(function () {
sandbox.stub(DefaultStsClient.prototype, 'assumeRole').callsFake(async (request) => {
assert.strictEqual(request.RoleArn, 'testarn')
if (request.SerialNumber) {
assert.strictEqual(request.SerialNumber, 'mfaSerialToken')
assert.strictEqual(request.TokenCode, 'mfaToken')
}
return {
Credentials: {
AccessKeyId: 'id',
SecretAccessKey: 'secret',
SessionToken: 'token',
Expiration: new Date(Date.now() + oneDay),
},
}
})
})

it('assumes role given in ini data', async function () {
const sections = await createTestSections(`
${defaultSection}
[profile assume]
source_profile = default
role_arn = testarn
`)

const sut = new SharedCredentialsProvider('assume', sections)
const creds = await sut.getCredentials()
assert.strictEqual(creds.accessKeyId, 'id')
assert.strictEqual(creds.secretAccessKey, 'secret')
assert.strictEqual(creds.sessionToken, 'token')
})

it('assumes role with mfa token', async function () {
const sections = await createTestSections(`
${defaultSection}
[profile assume]
source_profile = default
role_arn = testarn
mfa_serial= mfaSerialToken
`)
const sut = new SharedCredentialsProvider('assume', sections)

getTestWindow().onDidShowInputBox((inputBox) => {
inputBox.acceptValue('mfaToken')
})

const creds = await sut.getCredentials()
assert.strictEqual(creds.accessKeyId, 'id')
assert.strictEqual(creds.secretAccessKey, 'secret')
assert.strictEqual(creds.sessionToken, 'token')
})

it('does not assume role when no roleArn is present', async function () {
const sut = new SharedCredentialsProvider('default', await createTestSections(defaultSection))
const creds = await sut.getCredentials()
assert.strictEqual(creds.accessKeyId, 'x')
assert.strictEqual(creds.secretAccessKey, 'y')
})
})
})

function assertSubstringsInText(text: string | undefined, ...substrings: string[]) {
Expand Down
Loading