Skip to content

Commit 611c48d

Browse files
authored
feat(cli): MFA support (#6510)
With these changes AWS CDK now supports `mfa_serial` field so when profile has `mfa_serial` set the user is asked for MFA token. If user then adds corrects short lived token they will get access to environment. Example config for assume role with MFA that will be supported after these changes. ``` [profile mfa] region=eu-west-1 [profile mfa-role] source_profile=mfa role_arn=arn:aws:iam::account:role/role mfa_serial=arn:aws:iam::account:mfa/user ``` These changes currently only have one test as I don't have enough knowledge of the code base to write better tests. Current test only checks that user is asked for token by looking at the error message which should result in invalid token. Fixes: #1248 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* <!-- Please read the contribution guidelines and follow the pull-request checklist: https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md -->
1 parent d6c44e8 commit 611c48d

File tree

3 files changed

+67
-3
lines changed

3 files changed

+67
-3
lines changed

packages/aws-cdk/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,19 @@ $ cdk doctor
266266
- AWS_SDK_LOAD_CONFIG = 1
267267
```
268268

269+
### MFA support
270+
271+
If `mfa_serial` is found in the active profile of the shared ini file AWS CDK
272+
will ask for token defined in the `mfa_serial`. This token will be provided to STS assume role call.
273+
274+
Example profile in `~/.aws/config` where `mfa_serial` is used to assume role:
275+
```ini
276+
[profile my_assume_role_profile]
277+
source_profile=my_source_role
278+
role_arn=arn:aws:iam::123456789123:role/role_to_be_assumed
279+
mfa_serial=arn:aws:iam::123456789123:mfa/my_user
280+
```
281+
269282
### Configuration
270283
On top of passing configuration through command-line arguments, it is possible to use JSON configuration files. The
271284
configuration's order of precedence is:

packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from 'path';
44
import * as util from 'util';
55
import * as AWS from 'aws-sdk';
66
import * as fs from 'fs-extra';
7+
import * as promptly from 'promptly';
78
import { debug } from '../../logging';
89
import { SharedIniFile } from './sdk_ini_file';
910

@@ -45,11 +46,11 @@ export class AwsCliCompatible {
4546
];
4647

4748
if (await fs.pathExists(credentialsFileName())) {
48-
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
49+
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions, tokenCodeFn }));
4950
}
5051

5152
if (await fs.pathExists(configFileName())) {
52-
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
53+
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions, tokenCodeFn }));
5354
}
5455

5556
if (containerCreds ?? hasEcsCredentials()) {
@@ -200,4 +201,24 @@ function readIfPossible(filename: string): string | undefined {
200201
debug(e);
201202
return undefined;
202203
}
203-
}
204+
}
205+
206+
/**
207+
* Ask user for MFA token for given serial
208+
*
209+
* Result is send to callback function for SDK to authorize the request
210+
*/
211+
async function tokenCodeFn(serialArn: string, cb: (err?: Error, token?: string) => void): Promise<void> {
212+
debug('Require MFA token for serial ARN', serialArn);
213+
try {
214+
const token: string = await promptly.prompt(`MFA token for ${serialArn}: `, {
215+
trim: true,
216+
default: '',
217+
});
218+
debug('Successfully got MFA token from user');
219+
cb(undefined, token);
220+
} catch (err) {
221+
debug('Failed to get MFA token', err);
222+
cb(err);
223+
}
224+
}

packages/aws-cdk/test/api/sdk-provider.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { ISDK, Mode, SdkProvider } from '../../lib/api/aws-auth';
88
import * as logging from '../../lib/logging';
99
import * as bockfs from '../bockfs';
1010

11+
// Mock promptly prompt to test MFA support
12+
jest.mock('promptly', () => ({
13+
prompt: jest.fn().mockRejectedValue(new Error('test')),
14+
}));
15+
1116
SDKMock.setSDKInstance(AWS);
1217

1318
type AwsCallback<T> = (err: Error | null, val: T) => void;
@@ -40,6 +45,10 @@ beforeEach(() => {
4045
[assumer]
4146
aws_access_key_id=${uid}assumer
4247
aws_secret_access_key=secret
48+
49+
[mfa]
50+
aws_access_key_id=${uid}mfaccess
51+
aws_secret_access_key=secret
4352
`),
4453
'/home/me/.bxt/config': dedent(`
4554
[default]
@@ -59,6 +68,14 @@ beforeEach(() => {
5968
6069
[profile assumer]
6170
region=us-east-2
71+
72+
[profile mfa]
73+
region=eu-west-1
74+
75+
[profile mfa-role]
76+
source_profile=mfa
77+
role_arn=arn:aws:iam::account:role/role
78+
mfa_serial=arn:aws:iam::account:mfa/user
6279
`),
6380
});
6481

@@ -150,6 +167,19 @@ describe('CLI compatible credentials loading', () => {
150167
expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}booccess`);
151168
});
152169

170+
test('mfa_serial in profile will ask user for token', async () => {
171+
// WHEN
172+
const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'mfa-role' });
173+
174+
// THEN
175+
try {
176+
await provider.withAssumedRole('arn:aws:iam::account:role/role', undefined, undefined);
177+
} catch (e) {
178+
// Mock response was set to fail with message test to make sure we don't call STS
179+
expect(e.message).toEqual('Error fetching MFA token: test');
180+
}
181+
});
182+
153183
test('different account throws', async () => {
154184
const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' });
155185

0 commit comments

Comments
 (0)