mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Add support for AWS authentication (#347)
* Implements #344 * Multifactor authentication (MFA) not yet supported
This commit is contained in:
parent
5891414d0b
commit
d2c677c0fd
@ -139,6 +139,7 @@ export const AUTH_BASIC = 'basic';
|
||||
export const AUTH_DIGEST = 'digest';
|
||||
export const AUTH_BEARER = 'bearer';
|
||||
export const AUTH_NTLM = 'ntlm';
|
||||
export const AUTH_AWS_IAM = 'iam';
|
||||
|
||||
const authTypesMap = {
|
||||
[AUTH_BASIC]: ['Basic', 'Basic Auth'],
|
||||
@ -146,7 +147,8 @@ const authTypesMap = {
|
||||
[AUTH_NTLM]: ['NTLM', 'Microsoft NTLM'],
|
||||
[AUTH_BEARER]: ['Bearer', 'Bearer Token'],
|
||||
[AUTH_OAUTH_1]: ['OAuth 1', 'OAuth 1.0'],
|
||||
[AUTH_OAUTH_2]: ['OAuth 2', 'OAuth 2.0']
|
||||
[AUTH_OAUTH_2]: ['OAuth 2', 'OAuth 2.0'],
|
||||
[AUTH_AWS_IAM]: ['AWS', 'AWS IAM v4']
|
||||
};
|
||||
|
||||
export function getPreviewModeName (previewMode, useLong = false) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_2, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_OTHER, getContentTypeFromHeaders, METHOD_GET} from '../common/constants';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_2, AUTH_AWS_IAM, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_OTHER, getContentTypeFromHeaders, METHOD_GET} from '../common/constants';
|
||||
import * as db from '../common/database';
|
||||
import {getContentTypeHeader} from '../common/misc';
|
||||
import {buildFromParams, deconstructToParams} from '../common/querystring';
|
||||
@ -50,6 +50,14 @@ export function newAuth (type, oldAuth = {}) {
|
||||
case AUTH_OAUTH_2:
|
||||
return {type, grantType: GRANT_TYPE_AUTHORIZATION_CODE};
|
||||
|
||||
case AUTH_AWS_IAM:
|
||||
return {
|
||||
type,
|
||||
disabled: oldAuth.disabled || false,
|
||||
accessKeyId: oldAuth.accessKeyId || '',
|
||||
secretAccessKey: oldAuth.secretAccessKey || ''
|
||||
};
|
||||
|
||||
// Types needing no defaults
|
||||
default:
|
||||
return {type};
|
||||
|
@ -3,7 +3,8 @@ import * as db from '../../common/database';
|
||||
import {join as pathJoin, resolve as pathResolve} from 'path';
|
||||
import {getRenderedRequest} from '../../common/render';
|
||||
import * as models from '../../models';
|
||||
import {AUTH_BASIC, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../../common/constants';
|
||||
import {AUTH_BASIC, AUTH_AWS_IAM, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../../common/constants';
|
||||
import {filterHeaders} from '../../common/misc';
|
||||
|
||||
describe('actuallySend()', () => {
|
||||
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
|
||||
@ -400,3 +401,46 @@ describe('actuallySend()', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getAwsAuthHeaders', () => {
|
||||
it('should generate expected headers', () => {
|
||||
const req = {
|
||||
authentication: {
|
||||
type: AUTH_AWS_IAM,
|
||||
accessKeyId: 'AKIA99999999',
|
||||
secretAccessKey: 'SAK9999999999999'
|
||||
},
|
||||
headers: [{name: 'content-type', value: 'application/json'}],
|
||||
body: {text: '{}'}
|
||||
};
|
||||
const url = 'https://example.com/path?query=q1';
|
||||
const headers = networkUtils._getAwsAuthHeaders(req, url);
|
||||
expect(filterHeaders(headers, 'x-amz-date')[0].value)
|
||||
.toMatch(/^\d{8}T\d{6}Z$/);
|
||||
expect(filterHeaders(headers, 'host')[0].value).toEqual('example.com');
|
||||
expect(filterHeaders(headers, 'authorization')[0].value)
|
||||
.toMatch(/^AWS4-HMAC-SHA256 Credential=AKIA99999999/);
|
||||
expect(filterHeaders(headers, 'content-type'))
|
||||
.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle sparse request', () => {
|
||||
const req = {
|
||||
authentication: {
|
||||
type: AUTH_AWS_IAM,
|
||||
accessKeyId: 'AKIA99999999',
|
||||
secretAccessKey: 'SAK9999999999999'
|
||||
},
|
||||
headers: []
|
||||
};
|
||||
const url = 'https://example.com';
|
||||
const headers = networkUtils._getAwsAuthHeaders(req, url);
|
||||
expect(filterHeaders(headers, 'x-amz-date')[0].value)
|
||||
.toMatch(/^\d{8}T\d{6}Z$/);
|
||||
expect(filterHeaders(headers, 'host')[0].value).toEqual('example.com');
|
||||
expect(filterHeaders(headers, 'authorization')[0].value)
|
||||
.toMatch(/^AWS4-HMAC-SHA256 Credential=AKIA99999999/);
|
||||
expect(filterHeaders(headers, 'content-type'))
|
||||
.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ import {join as pathJoin} from 'path';
|
||||
import * as models from '../models';
|
||||
import * as querystring from '../common/querystring';
|
||||
import * as util from '../common/misc.js';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_NTLM, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../common/constants';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_NTLM, AUTH_AWS_IAM, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../common/constants';
|
||||
import {describeByteSize, hasAuthHeader, hasContentTypeHeader, hasUserAgentHeader, setDefaultProtocol} from '../common/misc';
|
||||
import {getRenderedRequest} from '../common/render';
|
||||
import fs from 'fs';
|
||||
@ -16,6 +16,7 @@ import * as CACerts from './cacert';
|
||||
import {getAuthHeader} from './authentication';
|
||||
import {cookiesFromJar, jarFromCookies} from '../common/cookies';
|
||||
import urlMatchesCertHost from './url-matches-cert-host';
|
||||
import aws4 from 'aws4';
|
||||
|
||||
// Time since user's last keypress to wait before making the request
|
||||
const MAX_DELAY_TIME = 1000;
|
||||
@ -383,6 +384,13 @@ export function _actuallySend (renderedRequest, workspace, settings) {
|
||||
setOpt(Curl.option.HTTPAUTH, Curl.auth.NTLM);
|
||||
setOpt(Curl.option.USERNAME, username || '');
|
||||
setOpt(Curl.option.PASSWORD, password || '');
|
||||
} else if (renderedRequest.authentication.type === AUTH_AWS_IAM) {
|
||||
if (!renderedRequest.body.text) {
|
||||
throw new Error('AWS authentication not supported for provided body type');
|
||||
}
|
||||
_getAwsAuthHeaders(renderedRequest, finalUrl).forEach((header) => {
|
||||
headers.push(header);
|
||||
});
|
||||
} else {
|
||||
const authHeader = await getAuthHeader(
|
||||
renderedRequest._id,
|
||||
@ -562,6 +570,33 @@ function _getCurlHeader (curlHeadersObj, name, fallback) {
|
||||
}
|
||||
}
|
||||
|
||||
// exported for unit tests only
|
||||
export function _getAwsAuthHeaders (req, url) {
|
||||
const credentials = {
|
||||
accessKeyId: req.authentication.accessKeyId,
|
||||
secretAccessKey: req.authentication.secretAccessKey
|
||||
};
|
||||
const parsedUrl = urlParse(url);
|
||||
const contentTypeHeader = util.getContentTypeHeader(req.headers);
|
||||
const options = {
|
||||
path: parsedUrl.path,
|
||||
// hostname is what we want here whether your URL has port or not
|
||||
host: parsedUrl.hostname,
|
||||
headers: {
|
||||
'content-type': contentTypeHeader ? contentTypeHeader.value : ''
|
||||
}
|
||||
};
|
||||
const body = req.body && req.body.text;
|
||||
if (body) {
|
||||
options.body = body;
|
||||
}
|
||||
const signature = aws4.sign(options, credentials);
|
||||
return Object.keys(signature.headers)
|
||||
// Don't use the inferred content type aws4 provides
|
||||
.filter((name) => name !== 'content-type')
|
||||
.map((name) => ({name, value: signature.headers[name]}));
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
return;
|
||||
|
@ -5,7 +5,7 @@ import {trackEvent} from '../../../analytics';
|
||||
import {showModal} from '../modals';
|
||||
import AlertModal from '../modals/alert-modal';
|
||||
import * as models from '../../../models';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2, getAuthTypeName} from '../../../common/constants';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2, AUTH_AWS_IAM, getAuthTypeName} from '../../../common/constants';
|
||||
|
||||
@autobind
|
||||
class AuthDropdown extends PureComponent {
|
||||
@ -67,6 +67,7 @@ class AuthDropdown extends PureComponent {
|
||||
{this.renderAuthType(AUTH_DIGEST)}
|
||||
{this.renderAuthType(AUTH_BEARER)}
|
||||
{this.renderAuthType(AUTH_NTLM)}
|
||||
{this.renderAuthType(AUTH_AWS_IAM)}
|
||||
<DropdownDivider>Other</DropdownDivider>
|
||||
{this.renderAuthType(AUTH_NONE, 'No Authentication')}
|
||||
</Dropdown>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2} from '../../../../common/constants';
|
||||
import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2, AUTH_AWS_IAM} from '../../../../common/constants';
|
||||
import BasicAuth from './basic-auth';
|
||||
import DigestAuth from './digest-auth';
|
||||
import BearerAuth from './bearer-auth';
|
||||
import NTLMAuth from './ntlm-auth';
|
||||
import OAuth2 from './o-auth-2';
|
||||
import AWSAuth from './aws-auth';
|
||||
import autobind from 'autobind-decorator';
|
||||
import Link from '../../base/link';
|
||||
|
||||
@ -92,6 +93,17 @@ class AuthWrapper extends PureComponent {
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
} else if (authentication.type === AUTH_AWS_IAM) {
|
||||
return (
|
||||
<AWSAuth
|
||||
authentication={authentication}
|
||||
handleRender={handleRender}
|
||||
handleGetRenderContext={handleGetRenderContext}
|
||||
handleUpdateSettingsShowPasswords={handleUpdateSettingsShowPasswords}
|
||||
onChange={onChange}
|
||||
showPasswords={showPasswords}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="vertically-center text-center">
|
||||
|
75
app/ui/components/editors/auth/aws-auth.js
Normal file
75
app/ui/components/editors/auth/aws-auth.js
Normal file
@ -0,0 +1,75 @@
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import KeyValueEditor from '../../key-value-editor/editor';
|
||||
import {trackEvent} from '../../../../analytics/index';
|
||||
import {AUTH_AWS_IAM} from '../../../../common/constants';
|
||||
|
||||
@autobind
|
||||
class AWSAuth extends PureComponent {
|
||||
_handleOnCreate () {
|
||||
trackEvent('AWS Auth Editor', 'Create');
|
||||
}
|
||||
|
||||
_handleOnDelete () {
|
||||
trackEvent('AWS Auth Editor', 'Delete');
|
||||
}
|
||||
|
||||
_handleToggleDisable (pair) {
|
||||
const label = pair.disabled ? 'Disable' : 'Enable';
|
||||
trackEvent('AWS Auth Editor', 'Toggle', label);
|
||||
}
|
||||
|
||||
_handleChange (pairs) {
|
||||
const pair = {
|
||||
type: AUTH_AWS_IAM,
|
||||
accessKeyId: pairs.length ? pairs[0].name : '',
|
||||
secretAccessKey: pairs.length ? pairs[0].value : '',
|
||||
disabled: pairs.length ? pairs[0].disabled : false
|
||||
};
|
||||
|
||||
this.props.onChange(pair);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
authentication,
|
||||
showPasswords,
|
||||
handleRender,
|
||||
handleGetRenderContext
|
||||
} = this.props;
|
||||
|
||||
const pairs = [{
|
||||
name: authentication.accessKeyId || '',
|
||||
value: authentication.secretAccessKey || '',
|
||||
disabled: authentication.disabled || false
|
||||
}];
|
||||
|
||||
return (
|
||||
<KeyValueEditor
|
||||
pairs={pairs}
|
||||
maxPairs={1}
|
||||
disableDelete
|
||||
handleRender={handleRender}
|
||||
handleGetRenderContext={handleGetRenderContext}
|
||||
namePlaceholder="AWS_ACCESS_KEY_ID"
|
||||
valuePlaceholder="AWS_SECRET_ACCESS_KEY"
|
||||
valueInputType={showPasswords ? 'text' : 'password'}
|
||||
onToggleDisable={this._handleToggleDisable}
|
||||
onCreate={this._handleOnCreate}
|
||||
onDelete={this._handleOnDelete}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AWSAuth.propTypes = {
|
||||
handleRender: PropTypes.func.isRequired,
|
||||
handleGetRenderContext: PropTypes.func.isRequired,
|
||||
handleUpdateSettingsShowPasswords: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
authentication: PropTypes.object.isRequired,
|
||||
showPasswords: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AWSAuth;
|
@ -109,6 +109,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"autobind-decorator": "^1.3.4",
|
||||
"aws4": "1.6.0",
|
||||
"classnames": "^2.2.5",
|
||||
"clone": "^2.1.0",
|
||||
"codemirror": "^5.24.2",
|
||||
|
Loading…
Reference in New Issue
Block a user