Skip to content

Commit

Permalink
feat(cli): MFA support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nikolauska committed May 24, 2020
1 parent a721e67 commit 5ea8701
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 29 deletions.
13 changes: 13 additions & 0 deletions packages/aws-cdk/README.md
Expand Up @@ -253,6 +253,19 @@ $ cdk doctor
- AWS_SDK_LOAD_CONFIG = 1
```

### MFA support

If `mfa_serial` is found in the active profile of the shared ini file AWS CDK
will ask for token defined in the `mfa_serial`. This token will be provided to STS assume role call.

Example profile in `~/.aws/config` where `mfa_serial` is used to assume role:
```ini
[profile my_assume_role_profile]
source_profile=my_source_role
role_arn=arn:aws:iam::123456789123:role/role_to_be_assumed
mfa_serial=arn:aws:iam::123456789123:mfa/my_user
```

### Configuration
On top of passing configuration through command-line arguments, it is possible to use JSON configuration files. The
configuration's order of precedence is:
Expand Down
27 changes: 24 additions & 3 deletions packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts
Expand Up @@ -3,6 +3,7 @@ import * as child_process from 'child_process';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as promptly from 'promptly';
import * as util from 'util';
import { debug } from '../../logging';
import { SharedIniFile } from './sdk_ini_file';
Expand Down Expand Up @@ -45,11 +46,11 @@ export class AwsCliCompatible {
];

if (await fs.pathExists(credentialsFileName())) {
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions, tokenCodeFn }));
}

if (await fs.pathExists(configFileName())) {
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions }));
sources.push(() => new AWS.SharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions, tokenCodeFn }));
}

if (containerCreds ?? hasEcsCredentials()) {
Expand Down Expand Up @@ -200,4 +201,24 @@ function readIfPossible(filename: string): string | undefined {
debug(e);
return undefined;
}
}
}

/**
* Ask user for MFA token for given serial
*
* Result is send to callback function for SDK to authorize the request
*/
async function tokenCodeFn(serialArn: string, cb: (err?: Error, token?: string) => void): Promise<void> {
debug('Require MFA token for serial ARN', serialArn);
try {
const token: string = await promptly.prompt(`MFA token for ${serialArn}: `, {
trim: true,
default: '',
});
debug('Successfully got MFA token from user');
cb(undefined, token);
} catch (err) {
debug('Failed to get MFA token', err);
cb(err);
}
}
2 changes: 1 addition & 1 deletion packages/aws-cdk/package.json
Expand Up @@ -66,9 +66,9 @@
},
"dependencies": {
"@aws-cdk/cdk-assets-schema": "0.0.0",
"@aws-cdk/cloud-assembly-schema": "0.0.0",
"@aws-cdk/cloudformation-diff": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"@aws-cdk/cloud-assembly-schema": "0.0.0",
"@aws-cdk/region-info": "0.0.0",
"archiver": "^4.0.1",
"aws-sdk": "^2.681.0",
Expand Down
32 changes: 31 additions & 1 deletion packages/aws-cdk/test/api/sdk-provider.test.ts
Expand Up @@ -8,6 +8,11 @@ import { ISDK, Mode, SdkProvider } from '../../lib/api/aws-auth';
import * as logging from '../../lib/logging';
import * as bockfs from '../bockfs';

// Mock promptly prompt to test MFA support
jest.mock('promptly', () => ({
prompt: jest.fn().mockResolvedValue('123abc'),
}));

SDKMock.setSDKInstance(AWS);
logging.setVerbose(true);

Expand Down Expand Up @@ -39,6 +44,10 @@ beforeEach(() => {
[assumer]
aws_access_key_id=${uid}assumer
aws_secret_access_key=secret
[mfa]
aws_access_key_id=${uid}mfaccess
aws_secret_access_key=secret
`),
'/home/me/.bxt/config': dedent(`
[default]
Expand All @@ -58,6 +67,14 @@ beforeEach(() => {
[profile assumer]
region=us-east-2
[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
`),
});

Expand Down Expand Up @@ -147,6 +164,19 @@ describe('CLI compatible credentials loading', () => {
expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}booccess`);
});

test('mfa_serial in profile will ask user for token', async () => {
// WHEN
const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'mfa-role' });

// THEN
try {
await provider.withAssumedRole('arn:aws:iam::account:role/role', undefined, undefined);
} catch (e) {
// Mock token cannot work, but having this error means user was asked for MFA token
expect(e.message).toEqual('The security token included in the request is invalid.');
}
});

test('different account throws', async () => {
const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' });

Expand Down Expand Up @@ -239,4 +269,4 @@ function commonPrefix(a: string, b: string): string {
if (a[i] !== b[i]) { return a.substring(0, i); }
}
return a.substr(N);
}
}

0 comments on commit 5ea8701

Please sign in to comment.