Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): MFA support #6510

Merged
merged 2 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from 'path';
import * as util from 'util';
import * as AWS from 'aws-sdk';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
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);
}
}
30 changes: 30 additions & 0 deletions packages/aws-cdk/test/api/sdk-provider.test.ts
Original file line number Diff line number Diff line change
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().mockRejectedValue(new Error('test')),
}));

SDKMock.setSDKInstance(AWS);

type AwsCallback<T> = (err: Error | null, val: T) => void;
Expand Down Expand Up @@ -40,6 +45,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 @@ -59,6 +68,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 @@ -150,6 +167,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 response was set to fail with message test to make sure we don't call STS
expect(e.message).toEqual('Error fetching MFA token: test');
}
});

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

Expand Down