Skip to content

Commit 6f6a8c2

Browse files
authored
deps(auth): remove dependence on deprecated and outdated @aws-sdk/* packages. (#6474)
## Problem The auth code relies on old versions of `@aws-sdk/*` that have since been deprecated or are no longer backward compatible, making versions bumps impossible. - `@aws-sdk/credential-provider-imds` has since been [deprecated](https://www.npmjs.com/package/@aws-sdk/credential-provider-imds) - `fromIni` from `@aws-sdk/credential-provider-ini` no longer supports passing a `loadedConfig`. - `AssumeRoleParams` is no longer exported by `@aws-sdk/credential-provider-ini`. We need to be able to bump these `@aws-sdk/*` package versions to continue to consume newer generated clients. Being pinned to older versions is also a security risk. See #6439 for more information. ## Solution - write custom credentials provider to replace `fromIni` with `loadedConfig` option. - drop dependency on `@aws-sdk/credential-provider-ini` since its no longer used. - add direct dependency on `@aws-sdk/credential-provider-env` since this was installed as part of `@aws-sdk-credential-provider-ini` before. - Fix many (not all) of the deprecation warnings in auth code related to credentials provider. ### Custom Credentials Provider Before, we used `fromIni` with the `loadedConfig` option which allows us to avoid reading the config file from disk on each credentials fetch and allows us to merge the current credentials with those found in the `.ini` file. To achieve the same behavior without the `loadedConfig` option, we need to write our own credentials provider that supports MFA and role assumption, and returns the desired merged credentials, rather than reading from disk. ### Testing - Manually verify this role assumption works by following the steps [here](https://docs.aws.amazon.com/sdkref/latest/guide/access-assume-role.html). - Manually verify MFA works via adapting [this](https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-role.html#:~:text=This%20policy%20allows%20the%20user,they%20authenticate%20by%20using%20MFA.&text=Next%2C%20add%20a%20line%20to,by%20the%20role's%20trust%20policy.&text=The%20mfa_serial%20setting%20can%20take,command%20with%20this%20profile%20fails.&text=The%20second%20profile%20entry%2C%20role,%22:%20%5B%20%7B%20...). (Used DuoMobile) - Add unit tests with API calls stubbed. ## Future Work - There are two tests that can now be re-enabled because of this version bump, undoing db27ebb - The steps to test role assumption could become an integ/e2e test. Right now requires setting many resources up in console, but perhaps this can all be done by the SDKs with an account on admin access. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent cd85e10 commit 6f6a8c2

File tree

7 files changed

+17854
-12781
lines changed

7 files changed

+17854
-12781
lines changed

docs/faq-credentials.md

+4
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ Issue [aws-toolkit-vscode#3667](https://github.com/aws/aws-toolkit-vscode/issues
1313
2. Attempt to sign in again with AWS Builder ID
1414
3. If sign is is successful you can remove the old folder: `rm -rf ~/.aws/sso-OLD`
1515
1. Or revert the change: `mv ~/.aws/sso-OLD ~/.aws/sso`
16+
17+
### AWS Shared Credentials File
18+
19+
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).

package-lock.json

+17,726-12,753
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@
504504
"@aws-sdk/client-lambda": "^3.637.0",
505505
"@aws-sdk/client-sso": "^3.342.0",
506506
"@aws-sdk/client-sso-oidc": "^3.574.0",
507-
"@aws-sdk/credential-provider-ini": "3.46.0",
507+
"@aws-sdk/credential-provider-env": "3.696.0",
508508
"@aws-sdk/credential-provider-process": "3.37.0",
509509
"@aws-sdk/credential-provider-sso": "^3.345.0",
510510
"@aws-sdk/property-provider": "3.46.0",

packages/core/src/auth/providers/ec2CredentialsProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { Credentials } from '@aws-sdk/types'
7-
import { fromInstanceMetadata } from '@aws-sdk/credential-provider-imds'
7+
import { fromInstanceMetadata } from '@smithy/credential-provider-imds'
88
import { DefaultEc2MetadataClient } from '../../shared/clients/ec2MetadataClient'
99
import { Ec2MetadataClient } from '../../shared/clients/ec2MetadataClient'
1010
import { getLogger } from '../../shared/logger/logger'

packages/core/src/auth/providers/ecsCredentialsProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { Credentials, CredentialProvider } from '@aws-sdk/types'
7-
import { fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
7+
import { fromContainerMetadata } from '@smithy/credential-provider-imds'
88
import { EnvironmentVariables } from '../../shared/environmentVariables'
99
import { CredentialType } from '../../shared/telemetry/telemetry.gen'
1010
import { getStringHash } from '../../shared/utilities/textUtilities'

packages/core/src/auth/providers/sharedCredentialsProvider.ts

+48-25
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
*/
55

66
import * as AWS from '@aws-sdk/types'
7-
import { AssumeRoleParams, fromIni } from '@aws-sdk/credential-provider-ini'
87
import { fromProcess } from '@aws-sdk/credential-provider-process'
9-
import { ParsedIniData, SharedConfigFiles } from '@smithy/shared-ini-file-loader'
8+
import { ParsedIniData } from '@smithy/types'
109
import { chain } from '@aws-sdk/property-provider'
11-
import { fromInstanceMetadata, fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
10+
import { fromInstanceMetadata, fromContainerMetadata } from '@smithy/credential-provider-imds'
1211
import { fromEnv } from '@aws-sdk/credential-provider-env'
1312
import { getLogger } from '../../shared/logger/logger'
1413
import { getStringHash } from '../../shared/utilities/textUtilities'
@@ -29,9 +28,10 @@ import {
2928
Profile,
3029
Section,
3130
} from '../credentials/sharedCredentials'
32-
import { SectionName, SharedCredentialsKeys } from '../credentials/types'
31+
import { CredentialsData, SectionName, SharedCredentialsKeys } from '../credentials/types'
3332
import { SsoProfile, hasScopes, scopesSsoAccountAccess } from '../connection'
3433
import { builderIdStartUrl } from '../sso/constants'
34+
import { ToolkitError } from '../../shared/errors'
3535

3636
const credentialSources = {
3737
ECS_CONTAINER: 'EcsContainer',
@@ -378,18 +378,6 @@ export class SharedCredentialsProvider implements CredentialsProvider {
378378
}
379379

380380
private makeSharedIniFileCredentialsProvider(loadedCreds?: ParsedIniData): AWS.CredentialProvider {
381-
const assumeRole = async (credentials: AWS.Credentials, params: AssumeRoleParams) => {
382-
const region = this.getDefaultRegion() ?? 'us-east-1'
383-
const stsClient = new DefaultStsClient(region, credentials)
384-
const response = await stsClient.assumeRole(params)
385-
return {
386-
accessKeyId: response.Credentials!.AccessKeyId!,
387-
secretAccessKey: response.Credentials!.SecretAccessKey!,
388-
sessionToken: response.Credentials?.SessionToken,
389-
expiration: response.Credentials?.Expiration,
390-
}
391-
}
392-
393381
// Our credentials logic merges profiles from the credentials and config files but SDK v3 does not
394382
// This can cause odd behavior where the Toolkit can switch to a profile but not authenticate with it
395383
// So the workaround is to do give the SDK the merged profiles directly
@@ -399,15 +387,50 @@ export class SharedCredentialsProvider implements CredentialsProvider {
399387
(k) => this.getProfile(k)
400388
)
401389

402-
return fromIni({
403-
profile: this.profileName,
404-
mfaCodeProvider: async (mfaSerial) => await getMfaTokenFromUser(mfaSerial, this.profileName),
405-
roleAssumer: assumeRole,
406-
loadedConfig: Promise.resolve({
407-
credentialsFile: loadedCreds ?? profiles,
408-
configFile: {},
409-
} as SharedConfigFiles),
410-
})
390+
return async () => {
391+
const iniData = loadedCreds ?? profiles
392+
const profile: CredentialsData = iniData[this.profileName]
393+
if (!profile) {
394+
throw new ToolkitError(`auth: Profile ${this.profileName} not found`)
395+
}
396+
// No role to assume, return static credentials.
397+
if (!profile.role_arn) {
398+
return {
399+
accessKeyId: profile.aws_access_key_id!,
400+
secretAccessKey: profile.aws_secret_access_key!,
401+
sessionToken: profile.aws_session_token,
402+
}
403+
}
404+
if (!profile.source_profile || !iniData[profile.source_profile]) {
405+
throw new ToolkitError(
406+
`auth: Profile ${this.profileName} is missing source_profile for role assumption`
407+
)
408+
}
409+
// Use source profile to assume IAM role based on role ARN provided.
410+
const sourceProfile = iniData[profile.source_profile!]
411+
const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', {
412+
accessKeyId: sourceProfile.aws_access_key_id!,
413+
secretAccessKey: sourceProfile.aws_secret_access_key!,
414+
})
415+
// Prompt for MFA Token if needed.
416+
const assumeRoleReq = {
417+
RoleArn: profile.role_arn,
418+
RoleSessionName: 'AssumeRoleSession',
419+
...(profile.mfa_serial
420+
? {
421+
SerialNumber: profile.mfa_serial,
422+
TokenCode: await getMfaTokenFromUser(profile.mfa_serial, this.profileName),
423+
}
424+
: {}),
425+
}
426+
const assumeRoleRsp = await stsClient.assumeRole(assumeRoleReq)
427+
return {
428+
accessKeyId: assumeRoleRsp.Credentials!.AccessKeyId!,
429+
secretAccessKey: assumeRoleRsp.Credentials!.SecretAccessKey!,
430+
sessionToken: assumeRoleRsp.Credentials?.SessionToken,
431+
expiration: assumeRoleRsp.Credentials?.Expiration,
432+
}
433+
}
411434
}
412435

413436
private makeSourcedCredentialsProvider(): AWS.CredentialProvider {

packages/core/src/test/credentials/provider/sharedCredentialsProvider.test.ts

+73
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { SsoClient } from '../../../auth/sso/clients'
1313
import { stub } from '../../utilities/stubber'
1414
import { SsoAccessTokenProvider } from '../../../auth/sso/ssoAccessTokenProvider'
1515
import { createTestSections } from '../testUtil'
16+
import { DefaultStsClient } from '../../../shared/clients/stsClient'
17+
import { oneDay } from '../../../shared/datetime'
18+
import { getTestWindow } from '../../shared/vscode/window'
1619

1720
const missingPropertiesFragment = 'missing properties'
1821

@@ -450,6 +453,76 @@ describe('SharedCredentialsProvider', async function () {
450453
})
451454
})
452455
})
456+
457+
describe('makeSharedIniFileCredentialsProvider', function () {
458+
let defaultSection: string
459+
460+
before(function () {
461+
defaultSection = `[profile default]
462+
aws_access_key_id = x
463+
aws_secret_access_key = y`
464+
})
465+
466+
beforeEach(function () {
467+
sandbox.stub(DefaultStsClient.prototype, 'assumeRole').callsFake(async (request) => {
468+
assert.strictEqual(request.RoleArn, 'testarn')
469+
if (request.SerialNumber) {
470+
assert.strictEqual(request.SerialNumber, 'mfaSerialToken')
471+
assert.strictEqual(request.TokenCode, 'mfaToken')
472+
}
473+
return {
474+
Credentials: {
475+
AccessKeyId: 'id',
476+
SecretAccessKey: 'secret',
477+
SessionToken: 'token',
478+
Expiration: new Date(Date.now() + oneDay),
479+
},
480+
}
481+
})
482+
})
483+
484+
it('assumes role given in ini data', async function () {
485+
const sections = await createTestSections(`
486+
${defaultSection}
487+
[profile assume]
488+
source_profile = default
489+
role_arn = testarn
490+
`)
491+
492+
const sut = new SharedCredentialsProvider('assume', sections)
493+
const creds = await sut.getCredentials()
494+
assert.strictEqual(creds.accessKeyId, 'id')
495+
assert.strictEqual(creds.secretAccessKey, 'secret')
496+
assert.strictEqual(creds.sessionToken, 'token')
497+
})
498+
499+
it('assumes role with mfa token', async function () {
500+
const sections = await createTestSections(`
501+
${defaultSection}
502+
[profile assume]
503+
source_profile = default
504+
role_arn = testarn
505+
mfa_serial= mfaSerialToken
506+
`)
507+
const sut = new SharedCredentialsProvider('assume', sections)
508+
509+
getTestWindow().onDidShowInputBox((inputBox) => {
510+
inputBox.acceptValue('mfaToken')
511+
})
512+
513+
const creds = await sut.getCredentials()
514+
assert.strictEqual(creds.accessKeyId, 'id')
515+
assert.strictEqual(creds.secretAccessKey, 'secret')
516+
assert.strictEqual(creds.sessionToken, 'token')
517+
})
518+
519+
it('does not assume role when no roleArn is present', async function () {
520+
const sut = new SharedCredentialsProvider('default', await createTestSections(defaultSection))
521+
const creds = await sut.getCredentials()
522+
assert.strictEqual(creds.accessKeyId, 'x')
523+
assert.strictEqual(creds.secretAccessKey, 'y')
524+
})
525+
})
453526
})
454527

455528
function assertSubstringsInText(text: string | undefined, ...substrings: string[]) {

0 commit comments

Comments
 (0)