From 2fcf98536f77d2f3d66e2298f8fd6ae37835307e Mon Sep 17 00:00:00 2001 From: Julien Giovaresco Date: Mon, 21 Aug 2017 19:43:12 +0200 Subject: [PATCH] Implement Hawk Authentication (#446) * Implement Hawk Authentication * fix the missing label of inputs * Fix PR reviews --- app/common/constants.js | 2 + app/common/har.js | 6 +- app/models/request.js | 6 +- app/network/authentication.js | 20 +++- app/network/network.js | 2 + app/ui/components/dropdowns/auth-dropdown.js | 3 +- .../components/editors/auth/auth-wrapper.js | 12 +- app/ui/components/editors/auth/hawk-auth.js | 107 ++++++++++++++++++ package-lock.json | 94 +++++++++++---- package.json | 1 + 10 files changed, 225 insertions(+), 28 deletions(-) create mode 100644 app/ui/components/editors/auth/hawk-auth.js diff --git a/app/common/constants.js b/app/common/constants.js index 12e1dbc45b..45c14d8291 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_HAWK = 'hawk'; export const AUTH_AWS_IAM = 'iam'; export const AUTH_NETRC = 'netrc'; @@ -149,6 +150,7 @@ const authTypesMap = { [AUTH_BEARER]: ['Bearer', 'Bearer Token'], [AUTH_OAUTH_1]: ['OAuth 1', 'OAuth 1.0'], [AUTH_OAUTH_2]: ['OAuth 2', 'OAuth 2.0'], + [AUTH_HAWK]: ['Hawk', 'Hawk'], [AUTH_AWS_IAM]: ['AWS', 'AWS IAM v4'], [AUTH_NETRC]: ['Netrc', 'Netrc'] }; diff --git a/app/common/har.js b/app/common/har.js index df56b2350c..8b446a42f8 100644 --- a/app/common/har.js +++ b/app/common/har.js @@ -8,6 +8,8 @@ import {getAuthHeader} from '../network/authentication'; export async function exportHarWithRequest (renderedRequest, addContentLength = false) { let postData = ''; + const url = misc.prepareUrlForSending(renderedRequest.url); + if (renderedRequest.body.fileName) { try { postData = newBodyRaw(fs.readFileSync(renderedRequest.body.fileName, 'base64')); @@ -36,6 +38,8 @@ export async function exportHarWithRequest (renderedRequest, addContentLength = if (!misc.hasAuthHeader(renderedRequest.headers)) { const header = await getAuthHeader( renderedRequest._id, + url, + renderedRequest.method, renderedRequest.authentication ); header && renderedRequest.headers.push(header); @@ -43,7 +47,7 @@ export async function exportHarWithRequest (renderedRequest, addContentLength = return { method: renderedRequest.method, - url: misc.prepareUrlForSending(renderedRequest.url), + url, httpVersion: 'HTTP/1.1', cookies: getCookies(renderedRequest), headers: renderedRequest.headers, diff --git a/app/models/request.js b/app/models/request.js index 54ce9403ed..914ad44975 100644 --- a/app/models/request.js +++ b/app/models/request.js @@ -1,6 +1,6 @@ // @flow import type {BaseModel} from './index'; -import {AUTH_BASIC, AUTH_DIGEST, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_2, AUTH_AWS_IAM, AUTH_NETRC, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_OTHER, getContentTypeFromHeaders, METHOD_GET, CONTENT_TYPE_GRAPHQL, CONTENT_TYPE_JSON, METHOD_POST} from '../common/constants'; +import {AUTH_BASIC, AUTH_DIGEST, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_2, AUTH_HAWK, AUTH_AWS_IAM, AUTH_NETRC, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_OTHER, getContentTypeFromHeaders, METHOD_GET, CONTENT_TYPE_GRAPHQL, CONTENT_TYPE_JSON, METHOD_POST} from '../common/constants'; import * as db from '../common/database'; import {getContentTypeHeader} from '../common/misc'; import {buildFromParams, deconstructToParams} from '../common/querystring'; @@ -116,6 +116,10 @@ export function newAuth (type: string, oldAuth: RequestAuthentication = {}): Req case AUTH_NETRC: return {type}; + // hawk + case AUTH_HAWK: + return {type, algorithm: 'sha256'}; + // Types needing no defaults default: return {type}; diff --git a/app/network/authentication.js b/app/network/authentication.js index 8c36b65276..0ffe4d40c3 100644 --- a/app/network/authentication.js +++ b/app/network/authentication.js @@ -1,8 +1,9 @@ -import {AUTH_BASIC, AUTH_BEARER, AUTH_OAUTH_2} from '../common/constants'; +import {AUTH_BASIC, AUTH_BEARER, AUTH_OAUTH_2, AUTH_HAWK} from '../common/constants'; import {getBasicAuthHeader, getBearerAuthHeader} from '../common/misc'; import getOAuth2Token from './o-auth-2/get-token'; +import * as Hawk from 'hawk'; -export async function getAuthHeader (requestId, authentication) { +export async function getAuthHeader (requestId, url, method, authentication) { if (authentication.disabled) { return null; } @@ -27,6 +28,21 @@ export async function getAuthHeader (requestId, authentication) { } } + if (authentication.type === AUTH_HAWK) { + const {id, key, algorithm} = authentication; + + const header = Hawk.client.header( + url, + method, + {credentials: {id, key, algorithm}} + ); + + return { + name: 'Authorization', + value: header.field + }; + } + return null; } diff --git a/app/network/network.js b/app/network/network.js index 62e208d608..c9459b91a2 100644 --- a/app/network/network.js +++ b/app/network/network.js @@ -473,6 +473,8 @@ export function _actuallySend ( } else { const authHeader = await getAuthHeader( renderedRequest._id, + finalUrl, + renderedRequest.method, renderedRequest.authentication ); diff --git a/app/ui/components/dropdowns/auth-dropdown.js b/app/ui/components/dropdowns/auth-dropdown.js index f6035ab456..c88670b082 100644 --- a/app/ui/components/dropdowns/auth-dropdown.js +++ b/app/ui/components/dropdowns/auth-dropdown.js @@ -6,7 +6,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, AUTH_AWS_IAM, AUTH_NETRC, getAuthTypeName} from '../../../common/constants'; +import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2, AUTH_HAWK, AUTH_AWS_IAM, AUTH_NETRC, getAuthTypeName} from '../../../common/constants'; @autobind class AuthDropdown extends PureComponent { @@ -70,6 +70,7 @@ class AuthDropdown extends PureComponent { {this.renderAuthType(AUTH_NTLM)} {this.renderAuthType(AUTH_AWS_IAM)} {this.renderAuthType(AUTH_NETRC)} + {this.renderAuthType(AUTH_HAWK)} Other {this.renderAuthType(AUTH_NONE, 'No Authentication')} diff --git a/app/ui/components/editors/auth/auth-wrapper.js b/app/ui/components/editors/auth/auth-wrapper.js index f8ffac68ed..4a653b323e 100644 --- a/app/ui/components/editors/auth/auth-wrapper.js +++ b/app/ui/components/editors/auth/auth-wrapper.js @@ -1,11 +1,12 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2, AUTH_AWS_IAM, AUTH_NETRC} from '../../../../common/constants'; +import {AUTH_BASIC, AUTH_DIGEST, AUTH_BEARER, AUTH_NTLM, AUTH_OAUTH_1, AUTH_OAUTH_2, AUTH_AWS_IAM, AUTH_HAWK, AUTH_NETRC} from '../../../../common/constants'; import BasicAuth from './basic-auth'; import DigestAuth from './digest-auth'; import BearerAuth from './bearer-auth'; import NTLMAuth from './ntlm-auth'; import OAuth2Auth from './o-auth-2-auth'; +import HawkAuth from './hawk-auth'; import AWSAuth from './aws-auth'; import NetrcAuth from './netrc-auth'; import autobind from 'autobind-decorator'; @@ -49,6 +50,15 @@ class AuthWrapper extends PureComponent { showPasswords={showPasswords} /> ); + } else if (authentication.type === AUTH_HAWK) { + return ( + + ); } else if (authentication.type === AUTH_OAUTH_1) { return (
diff --git a/app/ui/components/editors/auth/hawk-auth.js b/app/ui/components/editors/auth/hawk-auth.js new file mode 100644 index 0000000000..2c7fc76df8 --- /dev/null +++ b/app/ui/components/editors/auth/hawk-auth.js @@ -0,0 +1,107 @@ +// @flow +import type { Request } from '../../../../models/request'; + +import React from 'react'; +import autobind from 'autobind-decorator'; +import OneLineEditor from '../../codemirror/one-line-editor'; +import * as misc from '../../../../common/misc'; + +@autobind +class HawkAuth extends React.PureComponent { + props: { + request: Request, + handleRender: Function, + handleGetRenderContext: Function, + onChange: Function + }; + + _handleChangeProperty: Function; + + constructor (props: any) { + super(props); + + this._handleChangeProperty = misc.debounce(this._handleChangeProperty, 500); + } + + _handleChangeProperty (property: string, value: string | boolean): void { + const {request} = this.props; + const authentication = Object.assign({}, request.authentication, {[property]: value}); + this.props.onChange(authentication); + } + + _handleChangeHawkAuthId (value: string): void { + this._handleChangeProperty('id', value); + } + + _handleChangeHawkAuthKey (value: string): void { + this._handleChangeProperty('key', value); + } + + _handleChangeAlgorithm (value: string): void { + this._handleChangeProperty('algorithm', value); + } + + renderHawkAuthenticationFields (): Array> { + const hawkAuthId = this.renderInputRow( + 'Hawk Auth ID', + 'id', + this._handleChangeHawkAuthId + ); + + const hawkAuthKey = this.renderInputRow( + 'Hawk Auth Key', + 'key', + this._handleChangeHawkAuthKey + ); + + const algorithm = this.renderInputRow( + 'Algorithm', + 'algorithm', + this._handleChangeAlgorithm + ); + + return [hawkAuthId, hawkAuthKey, algorithm]; + } + + renderInputRow (label: string, + property: string, + onChange: Function): React.Element<*> { + const {handleRender, handleGetRenderContext, request} = this.props; + const id = label.replace(/ /g, '-'); + return ( + + + + + +
+ +
+ + + ); + } + + render () { + const fields = this.renderHawkAuthenticationFields(); + + return ( +
+ + {fields} +
+
+ ); + } +} + +export default HawkAuth; diff --git a/package-lock.json b/package-lock.json index dd6d3faa6b..551873656b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1499,11 +1499,11 @@ "dev": true }, "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "requires": { - "hoek": "2.16.3" + "hoek": "4.2.0" } }, "boxen": { @@ -2478,11 +2478,21 @@ "integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=" }, "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "requires": { - "boom": "2.10.1" + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.0" + } + } } }, "crypto-browserify": { @@ -5610,14 +5620,14 @@ } }, "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.0.2" } }, "highlight.js": { @@ -5642,9 +5652,9 @@ } }, "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" }, "hoist-non-react-statics": { "version": "1.2.0", @@ -10811,6 +10821,22 @@ "uuid": "3.0.0" }, "dependencies": { + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, "form-data": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", @@ -10829,6 +10855,30 @@ "ajv": "4.11.8", "har-schema": "1.0.5" } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } } } }, @@ -11247,11 +11297,11 @@ "dev": true }, "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", + "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", "requires": { - "hoek": "2.16.3" + "hoek": "4.2.0" } }, "sockjs": { diff --git a/package.json b/package.json index 28a551c035..e6331afb51 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "electron-devtools-installer": "^2.2.0", "electron-squirrel-startup": "^1.0.0", "graphql": "^0.10.5", + "hawk": "^6.0.2", "highlight.js": "^9.12.0", "hkdf": "^0.0.2", "html-entities": "^1.2.0",