Skip to content

Commit d8755ce

Browse files
nikolauskaNiko Lehtovirta
authored and
Niko Lehtovirta
committed
feat(cli): MFA support
With this change AWS CDK supports MFA. Specifically it will support mfa_serial field in the profile config by asking user for MFA token for the mfa_serial field ARN. AWS SDK has support for this built in so only change is adding tokenCodeFn function to sharedIniCredentials options. Callback sent to that function is used to return token back to SDK. Inquirer package is used to create interactive prompt for user to type the MFA token.
1 parent 9adf55f commit d8755ce

File tree

4 files changed

+275
-27
lines changed

4 files changed

+275
-27
lines changed

packages/aws-cdk/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,19 @@ $ cdk doctor
263263
- AWS_SDK_LOAD_CONFIG = 1
264264
```
265265

266+
### MFA support
267+
268+
If `mfa_serial` is found in the active profile of the shared ini file AWS CDK
269+
will ask for token defined in the `mfa_serial`. This token will be provided to STS assume role call.
270+
271+
Example profile in `~/.aws/config` where `mfa_serial` is used to assume role:
272+
```ini
273+
[profile my_assume_role_profile]
274+
source_profile=my_source_role
275+
role_arn=arn:aws:iam::123456789123:role/role_to_be_assumed
276+
mfa_serial=arn:aws:iam::123456789123:mfa/my_user
277+
```
278+
266279
### Configuration
267280
On top of passing configuration through command-line arguments, it is possible to use JSON configuration files. The
268281
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
@@ -3,6 +3,7 @@ import * as child_process from 'child_process';
33
import * as fs from 'fs-extra';
44
import * as os from 'os';
55
import * as path from 'path';
6+
import * as promptly from 'promptly';
67
import * as util from 'util';
78
import { debug } from '../../logging';
89
import { SharedIniFile } from './sdk_ini_file';
@@ -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().mockResolvedValue('123abc'),
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 token cannot work, but having this error means user was asked for MFA token
179+
expect(e.message).toEqual('The security token included in the request is invalid.');
180+
}
181+
});
182+
153183
test('different account throws', async () => {
154184
const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' });
155185

0 commit comments

Comments
 (0)