Skip to content

Commit

Permalink
AWS Multi-Factor Authentication (#730)
Browse files Browse the repository at this point in the history
* Support AWS Multifactor Auth (#719)

  * Back end support

* Update to latest aws4 dep (#719)

* Mockup AWS MFA UI (#719)

* Add enabled to most auth panes

* Some more work
  • Loading branch information
gschier committed Jan 29, 2018
1 parent 3907915 commit 7041853
Show file tree
Hide file tree
Showing 15 changed files with 629 additions and 275 deletions.
3 changes: 2 additions & 1 deletion packages/insomnia-app/app/models/request.js
Expand Up @@ -130,7 +130,8 @@ export function newAuth (type: string, oldAuth: RequestAuthentication = {}): Req
type,
disabled: oldAuth.disabled || false,
accessKeyId: oldAuth.accessKeyId || '',
secretAccessKey: oldAuth.secretAccessKey || ''
secretAccessKey: oldAuth.secretAccessKey || '',
sessionToken: oldAuth.sessionToken || ''
};

// Hawk
Expand Down
23 changes: 16 additions & 7 deletions packages/insomnia-app/app/network/__tests__/network.test.js
Expand Up @@ -558,16 +558,21 @@ describe('_getAwsAuthHeaders', () => {
authentication: {
type: AUTH_AWS_IAM,
accessKeyId: 'AKIA99999999',
secretAccessKey: 'SAK9999999999999'
secretAccessKey: 'SAK9999999999999',
sessionToken: 'ST9999999999999999'
},
headers: [{name: 'content-type', value: 'application/json'}],
body: {text: '{}'},
method: 'POST',
url: 'https://example.com/path?query=q1'
};
const credentials = {
accessKeyId: req.authentication.accessKeyId || '',
secretAccessKey: req.authentication.secretAccessKey || '',
sessionToken: req.authentication.sessionToken || ''
};
const headers = networkUtils._getAwsAuthHeaders(
req.authentication.accessKeyId,
req.authentication.secretAccessKey,
credentials,
req.headers,
req.body.text,
req.url,
Expand All @@ -587,7 +592,8 @@ describe('_getAwsAuthHeaders', () => {
authentication: {
type: AUTH_AWS_IAM,
accessKeyId: 'AKIA99999999',
secretAccessKey: 'SAK9999999999999'
secretAccessKey: 'SAK9999999999999',
sessionToken: 'ST99999999999999'
},
headers: [
'Accept: */*',
Expand All @@ -596,10 +602,13 @@ describe('_getAwsAuthHeaders', () => {
url: 'https://example.com',
method: 'GET'
};

const credentials = {
accessKeyId: req.authentication.accessKeyId || '',
secretAccessKey: req.authentication.secretAccessKey || '',
sessionToken: req.authentication.sessionToken || ''
};
const headers = networkUtils._getAwsAuthHeaders(
req.authentication.accessKeyId,
req.authentication.secretAccessKey,
credentials,
req.headers,
null,
req.url,
Expand Down
13 changes: 7 additions & 6 deletions packages/insomnia-app/app/network/network.js
Expand Up @@ -493,9 +493,13 @@ export async function _actuallySend (
return handleError(
new Error('AWS authentication not supported for provided body type'));
}
const credentials = {
accessKeyId: renderedRequest.authentication.accessKeyId || '',
secretAccessKey: renderedRequest.authentication.secretAccessKey || '',
sessionToken: renderedRequest.authentication.sessionToken || ''
};
const extraHeaders = _getAwsAuthHeaders(
renderedRequest.authentication.accessKeyId || '',
renderedRequest.authentication.secretAccessKey || '',
credentials,
headers,
requestBody || '',
finalUrl,
Expand Down Expand Up @@ -853,15 +857,12 @@ export function _parseHeaders (

// exported for unit tests only
export function _getAwsAuthHeaders (
accessKeyId: string,
secretAccessKey: string,
credentials: Object,
headers: Array<RequestHeader>,
body: string,
url: string,
method: string
) {
const credentials = {accessKeyId, secretAccessKey};

const parsedUrl = urlParse(url);
const contentTypeHeader = getContentTypeHeader(headers);

Expand Down
7 changes: 5 additions & 2 deletions packages/insomnia-app/app/ui/components/base/button.js
Expand Up @@ -21,11 +21,13 @@ class Button extends PureComponent {
disabled,
tabIndex,
className,
type
type,
id
} = this.props;

return (
<button disabled={disabled}
id={id}
type={type}
tabIndex={tabIndex}
className={className}
Expand All @@ -47,7 +49,8 @@ Button.propTypes = {
onClick: PropTypes.func,
disabled: PropTypes.bool,
tabIndex: PropTypes.number,
type: PropTypes.string
type: PropTypes.string,
id: PropTypes.string
};

export default Button;
128 changes: 89 additions & 39 deletions packages/insomnia-app/app/ui/components/editors/auth/asap-auth.js
@@ -1,25 +1,53 @@
// @flow
import type {Request} from '../../../../models/request';
import type {RequestAuthentication} from '../../../../models/request';

import classnames from 'classnames';
import * as React from 'react';
import autobind from 'autobind-decorator';
import OneLineEditor from '../../codemirror/one-line-editor';
import CodeEditor from '../../codemirror/code-editor';
import HelpTooltip from '../../help-tooltip';
import {showModal} from '../../modals';
import CodePromptModal from '../../modals/code-prompt-modal';
import Button from '../../base/button';

type Props = {
request: Request,
authentication: RequestAuthentication,
handleRender: Function,
handleGetRenderContext: Function,
nunjucksPowerUserMode: boolean,
onChange: Function
};

const PRIVATE_KEY_PLACEHOLDER = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA39k9udklHnmkU0GtTLpnYtKk1l5txYmUD/cGI0bFd3HHOOLG
mI0av55vMFEhxL7yrFrcL8pRKp0+pnOVStMDmbwsPE/pu9pf3uxD+m9/Flv89bUk
mml+R3E8PwAYzkX0cr4yQTPN9PSSqy+d2+KrZ9QZmpc3tqltTbMVV93cxKCxfBrf
jbiMIAVh7silDVY5+V46SJu8zY2kXOBBtlrE7/JoMiTURCkRjNIA8/sgSmRxBTdM
313lKJM7NgxaGnREbP75U7ErfBvReJsf5p6h5+XXFirG7F2ntcqjUoR3M+opngp0
CgffdGcsK7MmUUgAG7r05b0mljhI35t/0Y57MwIDAQABAoIBAQCH1rLohudJmROp
Gl/qAewfQiiZlfATQavCDGuDGL1YAIme8a8GgApNYf2jWnidhiqJgRHBRor+yzFr
cJV+wRTs/Szp6LXAgMmTkKMJ+9XXErUIUgwbl27Y3Rv/9ox1p5VRg+A=
-----END RSA PRIVATE KEY-----
`.trim();

@autobind
class AsapAuth extends React.PureComponent<Props> {
_handleDisable () {
const {authentication} = this.props;
authentication.disabled = !authentication.disabled;
this.props.onChange(authentication);
}

_handleChangeProperty (property: string, value: string | boolean): void {
const {request} = this.props;
const authentication = Object.assign({}, request.authentication, {[property]: value});
const {authentication} = this.props;
authentication[property] = value;
this.props.onChange(authentication);
}

_handleChangePrivateKey (value: string): void {
const {authentication} = this.props;
authentication.privateKey = value;
this.props.onChange(authentication);
}

Expand Down Expand Up @@ -54,8 +82,6 @@ class AsapAuth extends React.PureComponent<Props> {

const asapPrivateKey = this.renderPrivateKeyInput(
'Private Key',
'privateKey',
(value) => this._handleChangeProperty('privateKey', value)
);

return [asapIssuer, asapSubject, asapAudience, asapKeyId, asapPrivateKey];
Expand All @@ -67,7 +93,7 @@ class AsapAuth extends React.PureComponent<Props> {
placeholder: string,
onChange: Function
): React.Element<*> {
const { handleRender, handleGetRenderContext, request, nunjucksPowerUserMode } = this.props;
const {handleRender, handleGetRenderContext, authentication, nunjucksPowerUserMode} = this.props;
const id = label.replace(/ /g, '-');
return (
<tr key={id}>
Expand All @@ -77,12 +103,14 @@ class AsapAuth extends React.PureComponent<Props> {
</label>
</td>
<td className="wide">
<div className="form-control form-control--underlined no-margin">
<div className={classnames('form-control form-control--underlined no-margin', {
'form-control--inactive': authentication.disabled
})}>
<OneLineEditor
id={id}
placeholder={placeholder}
onChange={onChange}
defaultValue={request.authentication[property] || ''}
defaultValue={authentication[property] || ''}
nunjucksPowerUserMode={nunjucksPowerUserMode}
render={handleRender}
getRenderContext={handleGetRenderContext}
Expand All @@ -93,46 +121,44 @@ class AsapAuth extends React.PureComponent<Props> {
);
}

_handleEditPrivateKey () {
const {handleRender, handleGetRenderContext, authentication} = this.props;
showModal(CodePromptModal, {
submitName: 'Done',
title: `Edit Private Key`,
defaultValue: authentication.privateKey,
onChange: this._handleChangePrivateKey,
enableRender: handleRender || handleGetRenderContext,
placeholder: PRIVATE_KEY_PLACEHOLDER,
mode: 'text/plain',
hideMode: true
});
}

renderPrivateKeyInput (
label: string,
property: string,
onChange: Function
): React.Element<*> {
const { handleRender, handleGetRenderContext, request, nunjucksPowerUserMode } = this.props;
const id = label.replace(/ /g, '-');
const placeholderPrivateKey = '-----BEGIN RSA PRIVATE KEY-----\n' +
'MIIEpQIBAAKCAQEA39k9udklHnmkU0GtTLpnYtKk1l5txYmUD/cGI0bFd3HHOOLG\n' +
'mI0av55vMFEhxL7yrFrcL8pRKp0+pnOVStMDmbwsPE/pu9pf3uxD+m9/Flv89bUk\n' +
'mml+R3E8PwAYzkX0cr4yQTPN9PSSqy+d2+KrZ9QZmpc3tqltTbMVV93cxKCxfBrf\n' +
'jbiMIAVh7silDVY5+V46SJu8zY2kXOBBtlrE7/JoMiTURCkRjNIA8/sgSmRxBTdM\n' +
'313lKJM7NgxaGnREbP75U7ErfBvReJsf5p6h5+XXFirG7F2ntcqjUoR3M+opngp0\n' +
'CgffdGcsK7MmUUgAG7r05b0mljhI35t/0Y57MwIDAQABAoIBAQCH1rLohudJmROp\n' +
'Gl/qAewfQiiZlfATQavCDGuDGL1YAIme8a8GgApNYf2jWnidhiqJgRHBRor+yzFr\n' +
'cJV+wRTs/Szp6LXAgMmTkKMJ+9XXErUIUgwbl27Y3Rv/9ox1p5VRg+A=\n' +
'-----END RSA PRIVATE KEY-----';
const {authentication} = this.props;
return (
<tr key={id}>
<td className="pad-right pad-top-sm no-wrap valign-top">
<label htmlFor={id} className="label--small no-pad">
{label}
<HelpTooltip>Can also use single line data-uri format (e.g. obtained from asap-cli export-as-data-uri command), useful for saving as environment data</HelpTooltip>
<HelpTooltip>Can also use single line data-uri format (e.g. obtained from asap-cli
export-as-data-uri command), useful for saving as environment data</HelpTooltip>
</label>
</td>
<td className="wide">
<div className="form-control form-control--underlined form-control--tall no-margin">
<CodeEditor
id={id}
onChange={onChange}
defaultValue={request.authentication[property] || ''}
dynamicHeight={true}
hideLineNumbers={true}
lineWrapping={true}
placeholder={placeholderPrivateKey}
style={{height: 200}}
nunjucksPowerUserMode={nunjucksPowerUserMode}
render={handleRender}
getRenderContext={handleGetRenderContext}
/>
<div className={
classnames('form-control form-control--underlined form-control--tall no-margin', {
'form-control--inactive': authentication.disabled
})
}>
<button className="btn btn--clicky wide" onClick={this._handleEditPrivateKey}>
<i className="fa fa-edit space-right"/>
{authentication.privateKey ? 'Click to Edit' : 'Click to Add'}
</button>
</div>
</td>
</tr>
Expand All @@ -141,11 +167,35 @@ class AsapAuth extends React.PureComponent<Props> {

render () {
const fields = this.renderAsapAuthenticationFields();
const {authentication} = this.props;

return (
<div className="pad">
<table>
<tbody>{fields}</tbody>
<tbody>
{fields}
<tr>
<td className="pad-right no-wrap valign-middle">
<label htmlFor="enabled" className="label--small no-pad">
Enabled
</label>
</td>
<td className="wide">
<div className="form-control form-control--underlined">
<Button className="btn btn--super-duper-compact"
id="enabled"
onClick={this._handleDisable}
value={!authentication.disabled}
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
{authentication.disabled
? <i className="fa fa-square-o"/>
: <i className="fa fa-check-square-o"/>
}
</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
);
Expand Down
Expand Up @@ -57,7 +57,7 @@ class AuthWrapper extends PureComponent {
} else if (authentication.type === AUTH_HAWK) {
return (
<HawkAuth
request={request}
authentication={authentication}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={nunjucksPowerUserMode}
Expand Down Expand Up @@ -102,7 +102,6 @@ class AuthWrapper extends PureComponent {
return (
<BearerAuth
authentication={authentication}
request={request}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={nunjucksPowerUserMode}
Expand All @@ -128,7 +127,7 @@ class AuthWrapper extends PureComponent {
} else if (authentication.type === AUTH_ASAP) {
return (
<AsapAuth
request={request}
authentication={authentication}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={nunjucksPowerUserMode}
Expand Down

0 comments on commit 7041853

Please sign in to comment.