From d3e8a05fc78cf9ca2991d2d386f87cdbcb5b54a3 Mon Sep 17 00:00:00 2001 From: Roman Besolov Date: Mon, 6 Nov 2017 22:26:31 +0300 Subject: [PATCH] oAuth 1.0a Support (#571) --- app/models/request.js | 11 +- app/network/authentication.js | 15 +- app/network/o-auth-1/constants.js | 2 + app/network/o-auth-1/get-token.js | 42 +++++ .../components/editors/auth/auth-wrapper.js | 20 +- .../components/editors/auth/o-auth-1-auth.js | 173 ++++++++++++++++++ package-lock.json | 65 ++++--- package.json | 1 + 8 files changed, 285 insertions(+), 44 deletions(-) create mode 100644 app/network/o-auth-1/constants.js create mode 100644 app/network/o-auth-1/get-token.js create mode 100644 app/ui/components/editors/auth/o-auth-1-auth.js diff --git a/app/models/request.js b/app/models/request.js index c361bc08a3..8c97cb73f3 100644 --- a/app/models/request.js +++ b/app/models/request.js @@ -1,10 +1,11 @@ // @flow import type {BaseModel} from './index'; -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, HAWK_ALGORITHM_SHA256} from '../common/constants'; +import {AUTH_BASIC, AUTH_DIGEST, AUTH_NONE, AUTH_NTLM, AUTH_OAUTH_1, 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, HAWK_ALGORITHM_SHA256} from '../common/constants'; import * as db from '../common/database'; import {getContentTypeHeader} from '../common/misc'; import {buildFromParams, deconstructToParams} from '../common/querystring'; import {GRANT_TYPE_AUTHORIZATION_CODE} from '../network/o-auth-2/constants'; +import {SIGNATURE_METHOD_HMAC_SHA1} from '../network/o-auth-1/constants'; export const name = 'Request'; export const type = 'Request'; @@ -99,6 +100,14 @@ export function newAuth (type: string, oldAuth: RequestAuthentication = {}): Req password: oldAuth.password || '' }; + case AUTH_OAUTH_1: + return { + type, + disabled: false, + signatureMethod: SIGNATURE_METHOD_HMAC_SHA1, + version: '1.0' + }; + // OAuth 2.0 case AUTH_OAUTH_2: return {type, grantType: GRANT_TYPE_AUTHORIZATION_CODE}; diff --git a/app/network/authentication.js b/app/network/authentication.js index 0ffe4d40c3..6683ca5f50 100644 --- a/app/network/authentication.js +++ b/app/network/authentication.js @@ -1,6 +1,7 @@ -import {AUTH_BASIC, AUTH_BEARER, AUTH_OAUTH_2, AUTH_HAWK} from '../common/constants'; +import {AUTH_BASIC, AUTH_BEARER, AUTH_OAUTH_2, AUTH_OAUTH_1, AUTH_HAWK} from '../common/constants'; import {getBasicAuthHeader, getBearerAuthHeader} from '../common/misc'; import getOAuth2Token from './o-auth-2/get-token'; +import getOAuth1Token from './o-auth-1/get-token'; import * as Hawk from 'hawk'; export async function getAuthHeader (requestId, url, method, authentication) { @@ -28,6 +29,18 @@ export async function getAuthHeader (requestId, url, method, authentication) { } } + if (authentication.type === AUTH_OAUTH_1) { + const oAuth1Token = await getOAuth1Token(url, method, authentication); + if (oAuth1Token) { + return { + name: 'Authorization', + value: oAuth1Token.Authorization + }; + } else { + return null; + } + } + if (authentication.type === AUTH_HAWK) { const {id, key, algorithm} = authentication; diff --git a/app/network/o-auth-1/constants.js b/app/network/o-auth-1/constants.js new file mode 100644 index 0000000000..fdf9723d66 --- /dev/null +++ b/app/network/o-auth-1/constants.js @@ -0,0 +1,2 @@ +export const SIGNATURE_METHOD_HMAC_SHA1 = 'HMAC-SHA1'; +export const SIGNATURE_METHOD_PLAINTEXT = 'PLAINTEXT'; diff --git a/app/network/o-auth-1/get-token.js b/app/network/o-auth-1/get-token.js new file mode 100644 index 0000000000..30b3c62fb0 --- /dev/null +++ b/app/network/o-auth-1/get-token.js @@ -0,0 +1,42 @@ +/** + * Get an OAuth1Token object and also handle storing/saving/refreshing + * @returns {Promise.} + */ +import crypto from 'crypto'; +import OAuth1 from 'oauth-1.0a'; +import { SIGNATURE_METHOD_HMAC_SHA1, SIGNATURE_METHOD_PLAINTEXT } from './constants'; + +function hashFunction (signatureMethod) { + if (signatureMethod === SIGNATURE_METHOD_HMAC_SHA1) { + return function (baseString, key) { + return crypto.createHmac('sha1', key).update(baseString).digest('base64'); + }; + } + + if (signatureMethod === SIGNATURE_METHOD_PLAINTEXT) { + return function (baseString) { + return baseString; + }; + } + + return null; +} + +export default async function (url, method, authentication) { + var oauth = OAuth1({ + consumer: { + key: authentication.consumerKey, + secret: authentication.consumerSecret + }, + signature_method: authentication.signatureMethod, + version: authentication.version, + hash_function: hashFunction(authentication.signatureMethod) + }); + + var requestData = { + url: url, + method: method + }; + + return oauth.toHeader(oauth.authorize(requestData)); +} diff --git a/app/ui/components/editors/auth/auth-wrapper.js b/app/ui/components/editors/auth/auth-wrapper.js index 2676f2fdba..3d780095f3 100644 --- a/app/ui/components/editors/auth/auth-wrapper.js +++ b/app/ui/components/editors/auth/auth-wrapper.js @@ -6,11 +6,11 @@ import DigestAuth from './digest-auth'; import BearerAuth from './bearer-auth'; import NTLMAuth from './ntlm-auth'; import OAuth2Auth from './o-auth-2-auth'; +import OAuth1Auth from './o-auth-1-auth'; import HawkAuth from './hawk-auth'; import AWSAuth from './aws-auth'; import NetrcAuth from './netrc-auth'; import autobind from 'autobind-decorator'; -import Link from '../../base/link'; @autobind class AuthWrapper extends PureComponent { @@ -65,17 +65,13 @@ class AuthWrapper extends PureComponent { ); } else if (authentication.type === AUTH_OAUTH_1) { return ( -
-
- -

- Want OAuth 1.0? Please upvote - the - Issue on GitHub - -

-
-
+ ); } else if (authentication.type === AUTH_DIGEST) { return ( diff --git a/app/ui/components/editors/auth/o-auth-1-auth.js b/app/ui/components/editors/auth/o-auth-1-auth.js new file mode 100644 index 0000000000..ce9dde9363 --- /dev/null +++ b/app/ui/components/editors/auth/o-auth-1-auth.js @@ -0,0 +1,173 @@ +// @flow +import type {Request} from '../../../../models/request'; + +import * as React from 'react'; +import autobind from 'autobind-decorator'; +import OneLineEditor from '../../codemirror/one-line-editor'; +import * as misc from '../../../../common/misc'; +import HelpTooltip from '../../help-tooltip'; +import { SIGNATURE_METHOD_HMAC_SHA1, SIGNATURE_METHOD_PLAINTEXT } from '../../../../network/o-auth-1/constants'; + +type Props = { + handleRender: Function, + handleGetRenderContext: Function, + nunjucksPowerUserMode: boolean, + onChange: Function, + request: Request +}; + +@autobind +class OAuth1Auth extends React.PureComponent { + _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); + } + + _handleChangeConsumerKey (value: string): void { + this._handleChangeProperty('consumerKey', value); + } + + _handleChangeConsumerSecret (value: string): void { + this._handleChangeProperty('consumerSecret', value); + } + + _handleChangeSignatureMethod (e: SyntheticEvent): void { + this._handleChangeProperty('signatureMethod', e.currentTarget.value); + } + + _handleChangeVersion (value: string): void { + this._handleChangeProperty('version', value); + } + + renderInputRow ( + label: string, + property: string, + onChange: Function, + help: string | null = null, + handleAutocomplete: Function | null = null + ): React.Element<*> { + const {handleRender, handleGetRenderContext, request, nunjucksPowerUserMode} = this.props; + const id = label.replace(/ /g, '-'); + const type = !this.props.showPasswords && property === 'password' ? 'password' : 'text'; + return ( + + + + + +
+ +
+ + + ); + } + + renderSelectRow ( + label: string, + property: string, + options: Array<{name: string, value: string}>, + onChange: Function, + help: string | null = null + ): React.Element<*> { + const {request} = this.props; + const id = label.replace(/ /g, '-'); + const value = request.authentication.hasOwnProperty(property) + ? request.authentication[property] + : options[0]; + + return ( + + + + + +
+ +
+ + + ); + } + + renderFields (): Array> { + const consumerKey = this.renderInputRow( + 'Consumer Key', + 'consumerKey', + this._handleChangeConsumerKey + ); + + const consumerSecret = this.renderInputRow( + 'Consumer Secret', + 'consumerSecret', + this._handleChangeConsumerSecret + ); + + const signatureMethod = this.renderSelectRow( + 'Signature Method', + 'signatureMethod', + [ + {name: 'HMAC-SHA1', value: SIGNATURE_METHOD_HMAC_SHA1}, + {name: 'PLAINTEXT', value: SIGNATURE_METHOD_PLAINTEXT} + ], + this._handleChangeSignatureMethod + ); + + const version = this.renderInputRow( + 'Version', + 'version', + this._handleChangeVersion + ); + + return [ + consumerKey, + consumerSecret, + signatureMethod, + version + ]; + } + + render () { + const fields = this.renderFields(); + + return ( +
+ + + {fields} + +
+
+ ); + } +} + +export default OAuth1Auth; diff --git a/package-lock.json b/package-lock.json index 2a28e9b79f..2fa51aeada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,12 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/node": { - "version": "7.0.46", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz", - "integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==", - "dev": true - }, "7zip": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/7zip/-/7zip-0.0.6.tgz", @@ -31,6 +25,22 @@ "dev": true, "optional": true }, + "@types/node": { + "version": "7.0.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz", + "integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==", + "dev": true + }, + "JSONSelect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/JSONSelect/-/JSONSelect-0.4.0.tgz", + "integrity": "sha1-oI7cxn6z/L6Z7WMIVTRKDPKCu40=" + }, + "JSV": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", + "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=" + }, "a-sync-waterfall": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.0.tgz", @@ -5113,10 +5123,6 @@ } } }, - "string_decoder": { - "version": "0.10.31", - "bundled": true - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -5126,6 +5132,10 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "0.10.31", + "bundled": true + }, "stringstream": { "version": "0.0.5", "bundled": true, @@ -6697,10 +6707,6 @@ } } }, - "string_decoder": { - "version": "0.10.31", - "bundled": true - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -6710,6 +6716,10 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "0.10.31", + "bundled": true + }, "stringstream": { "version": "0.0.5", "bundled": true @@ -7590,12 +7600,12 @@ "resolved": "https://registry.npmjs.org/jison/-/jison-0.4.13.tgz", "integrity": "sha1-kEFwfWIkE2f1iDRTK58ZwsNvrHg=", "requires": { + "JSONSelect": "0.4.0", "cjson": "0.2.1", "ebnf-parser": "0.1.10", "escodegen": "0.0.21", "esprima": "1.0.4", "jison-lex": "0.2.1", - "JSONSelect": "0.4.0", "lex-parser": "0.1.4", "nomnom": "1.5.2" }, @@ -7961,11 +7971,6 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" }, - "JSONSelect": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/JSONSelect/-/JSONSelect-0.4.0.tgz", - "integrity": "sha1-oI7cxn6z/L6Z7WMIVTRKDPKCu40=" - }, "jsprim": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", @@ -8009,11 +8014,6 @@ } } }, - "JSV": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", - "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=" - }, "jsx-ast-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", @@ -9069,6 +9069,11 @@ "integrity": "sha1-tDiTYhcOfvl5jDx3FtgOvAEG/M8=", "dev": true }, + "oauth-1.0a": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.2.tgz", + "integrity": "sha512-fHRvq3aHoYXq8rZrXOX80nYgMc0svd4a88R4W7QLltJ1Rl+GFjW6Eu3rgR1zWB5MbuyqsWojADsS4vbmDiho/w==" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -11753,11 +11758,6 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", @@ -11783,6 +11783,11 @@ "integrity": "sha1-aybpvTr8qnvjtCabUm3huCAArHg=", "dev": true }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/package.json b/package.json index 170c975b89..787c881b8d 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "nedb": "^1.8.0", "node-forge": "^0.7.0", "nunjucks": "^3.0.0", + "oauth-1.0a": "^2.2.2", "pdfjs-dist": "^1.9.640", "prop-types": "^15.5.10", "react": "^16.0.0",