diff --git a/app/common/constants.js b/app/common/constants.js
index 1cf9d00a6..c6b818442 100644
--- a/app/common/constants.js
+++ b/app/common/constants.js
@@ -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) {
diff --git a/app/models/request.js b/app/models/request.js
index ebfa1aebc..950f3c469 100644
--- a/app/models/request.js
+++ b/app/models/request.js
@@ -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};
diff --git a/app/network/__tests__/network.test.js b/app/network/__tests__/network.test.js
index cb9b057e9..f4b4c6920 100644
--- a/app/network/__tests__/network.test.js
+++ b/app/network/__tests__/network.test.js
@@ -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);
+ });
+});
diff --git a/app/network/network.js b/app/network/network.js
index 91c6eb442..23c22153f 100644
--- a/app/network/network.js
+++ b/app/network/network.js
@@ -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;
diff --git a/app/ui/components/dropdowns/auth-dropdown.js b/app/ui/components/dropdowns/auth-dropdown.js
index 6a7b4574f..dadda5d53 100644
--- a/app/ui/components/dropdowns/auth-dropdown.js
+++ b/app/ui/components/dropdowns/auth-dropdown.js
@@ -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)}