Skip to content

Commit 5089db5

Browse files
authored
Merge branch 'master' into nija-at/apigw-accesslog-token
2 parents b050338 + 611c48d commit 5089db5

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)