api-console-assets
Version:
This repo only exists to publish api console components to npm
1,237 lines (1,188 loc) • 43.2 kB
HTML
<!--
@license
Copyright 2016 The Advanced REST client authors <arc@mulesoft.com>
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../cryptojs-lib/cryptojs-lib.html">
<link rel="import" href="../iron-meta/iron-meta.html">
<link rel="import" href="../url-parser/url-parser.html">
<link rel="import" href="jsrsasign-import.html">
<!--
An element to perform OAuth1 authorization and to sign auth requests.
Note that the OAuth1 authorization wasn't designed for browser. Most existing
OAuth1 implementation deisallow browsers to perform the authorization by
not allowing POST requests to authorization server. Therefore receiving token
may not be possible without using browser extensions to alter HTTP request to
enable CORS.
If the server disallow obtaining authorization token and secret from clients
then your application has to listen for `oauth1-token-requested` custom event
and perform authorization on the server side.
When auth token and secret is available and the user is to perform a HTTP request,
the request panel sends `before-request` cutom event. This element handles the event
and apllies authorization header with generated signature to the request.
## OAuth 1 configuration object
Both authorization or request signing requires detailed configuration object.
This is handled by the request panel. It sets OAuth1 configuration in the `request.auth`
property.
| Property | Type | Description |
| ----------------|-------------|---------- |
| `signatureMethod` | `String` | One of `PLAINTEXT`, `HMAC-SHA1`, `RSA-SHA1` |
| `requestTokenUrl` | `String` | Token request URI. Optional for before request. Required for authorization |
| `accessTokenUrl` | `String` | Access token request URI. Optional for before request. Required for authorization |
| `authorizationUrl` | `String` | User dialog URL. |
| `consumerKey` | `String` | Consumer key to be used to generate the signature. Optional for before request. |
| `consumerSecret` | `String` | Consumer secret to be used to generate the signature. Optional for before request. |
| `redirectUrl` | `String` | Redirect URI for the authorization. Optional for before request. |
| `authParamsLocation` | `String` | Optional. Location of the authorization parameters. Default to `authorization` meaning it creates an authorization header. Any other value means query parameters |
| `authTokenMethod` | `String` | Token request HTTP method. Default to `POST`. Optional for before request. |
| `version` | `String` | Oauth1 protocol version. Default to `1.0` |
| `nonceSize` | `Number` | Size of the nonce word to generate. Default to 32. Unused if `nonce` is set. |
| `nonce` | `String` | Nonce to be used to generate signature. |
| `timestamp` | `Number` | Request timestamp. If not set it sets current timestamp |
| `customHeaders` | `Object` | Map of custom headers to set with authorization request |
| `type` | `String` | Must be set to `oauth1` or during before-request this object will be ignored. |
| `token` | `String` | Required for signing requests. Received OAuth token |
| `tokenSecret` | `String` | Required for signing requests. Received OAuth token secret |
## Error codes
- `params-error` Oauth1 parameters are invalid
- `oauth1-error` OAuth popup is blocked.
- `token-request-error` HTTP request to the authorization server failed
- `no-response` No response recorded.
## Acknowledgements
- This element uses [jsrsasign](https://github.com/kjur/jsrsasign) library distributed
under MIT licence.
- This element uses [crypto-js](https://code.google.com/archive/p/crypto-js/) library
distributed under BSD license.
@group Logic Elements
@element oauth-authorization
-->
<script>
window.forceJURL = true;
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
// jscs:disable requireDotNotation
/* global UrlParser */
(function() {
'use strict';
Polymer({
is: 'oauth1-authorization',
properties: {
// A full data returned by the authorization endpoint.
tokenInfo: {
type: Object,
readOnly: true
},
/**
* If set, requests made by this element to authorization endpoint will be
* prefixed with the proxy value.
*/
proxy: String,
/**
* Latest valid token exchanged with the authorization endpoint.
*/
lastIssuedToken: {
type: Object,
notify: true
},
/**
* OAuth 1 token authorization endpoint.
*/
requestTokenUrl: String,
/**
* Oauth 1 token exchange endpoint
*/
accessTokenUrl: String,
/**
* Oauth 1 consumer key to use with auth request
*/
consumerKey: String,
/**
* Oauth 1 consumer secret to be used to generate the signature.
*/
consumerSecret: String,
/**
* A signature generation method.
* Once of: `PLAINTEXT`, `HMAC-SHA1` or `RSA-SHA1`
*/
signatureMethod: {
type: String,
value: 'HMAC-SHA1'
},
/**
* Location of the OAuth authorization parameters.
* It can be either `authorization` meaning as a header and
* `querystring` to put OAuth parameters to the URL.
*/
authParamsLocation: {
type: String,
value: 'authorization'
},
_caseMap: {
type: Object,
value: {}
},
_camelRegex: {
type: Object,
value: function() {
return /([A-Z])/g;
}
}
},
attached: function() {
this.listen(window, 'oauth1-token-requested', '_tokenRequestedHandler');
this.listen(window, 'message', '_listenPopup');
this.listen(window, 'before-request', '_handleRequest');
},
detached: function() {
this.unlisten(window, 'oauth1-token-requested', '_tokenRequestedHandler');
this.unlisten(window, 'message', '_listenPopup');
this.unlisten(window, 'before-request', '_handleRequest');
},
/**
* The `before-request` handler. Creates an authorization header if needed.
* Normally `before-request` expects to set a promise on the `detail.promises`
* object. But because this taks is sync it skips the promise and manipulate
* request object directly.
*/
_handleRequest: function(e) {
var request = e.detail;
if (!request.auth || request.auth.type !== 'oauth1') {
return;
}
this._applyBeforeRequestSignature(request, request.auth);
},
/**
* Applies OAuth1 authorization header with generated signature for this
* request.
*
* This method expects the `auth` object to be set on the request. The object
* is full configuration for the OAuth1 authorization as described in
* `auth-methods/oauth1.html` element.
*
* @param {Object} request ARC request object
* @param {String} token OAuth1 token
* @param {String} tokenSecret OAuth1 token secret
*/
_applyBeforeRequestSignature: function(request, auth) {
if (!request || !request.method || !request.url) {
return;
}
try {
this._prepareOauth(auth);
} catch (e) {
console.error(e);
return;
}
var token = auth.token || this.lastIssuedToken.oauth_token;
var tokenSecret = auth.tokenSecret || this.lastIssuedToken.oauth_token_secret;
var method = request.method || 'GET';
method = method.toUpperCase();
var withPayload = ['GET', 'HEAD'].indexOf(request.method) === -1;
var body;
if (withPayload && request.headers && request.body) {
var contentType = this.getContentType(request.headers);
if (contentType && contentType.indexOf('application/x-www-form-urlencoded') === 0) {
body = request.body;
}
}
var orderedParameters = this._prepareParameters(token, tokenSecret, method,
request.url, {}, body);
if (this.authParamsLocation === 'authorization') {
var authorization = this._buildAuthorizationHeaders(orderedParameters);
request.headers = this._replaceHeaderValue(request.headers, 'authorization', authorization);
} else {
request.url = this._buildAuthorizationQueryStirng(request.url, orderedParameters);
}
},
/**
* A handler for the `oauth1-token-requested` event.
* Performs OAuth1 authorization for given settings.
*
* The detail object of the event contains OAuth1 configuration as described
* in `auth-methods/oauth1.html`element.
*/
_tokenRequestedHandler: function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
this.authorize(e.detail);
},
/**
* Performs a request to authorization server.
*
* @param {Object} settings Oauth1 configuration. See description for more
* details or `auth-methods/oauth1.html` element that collectes configuration
* from the user.
*/
authorize: function(settings) {
try {
this._prepareOauth(settings);
} catch (e) {
console.error(e);
this.fire('oauth1-error', {
'message': 'Unable to authorize: ' + e.message,
'code': 'params-error'
});
return;
}
this.getOAuthRequestToken()
.then(function(temporaryCredentials) {
this.temporaryCredentials = temporaryCredentials;
var authorizationUrl = settings.authorizationUrl + '?oauth_token=' +
temporaryCredentials.oauth_token;
this.popupClosedProperly = undefined;
this._popup = window.open(authorizationUrl, 'api-console-oauth1');
if (!this._popup) {
// popup blocked.
this.fire('oauth1-error', {
'message': 'Authorization popup is blocked.',
'code': 'popup-blocked'
});
return;
}
this._next = 'exchange-token';
this._popup.window.focus();
this._observePopupState();
}.bind(this))
.catch(function(e) {
this.fire('oauth1-error', {
'message': e.message || 'Unknown error when getting the token',
'code': 'token-request-error'
});
}.bind(this));
},
/**
* Sets a configuration properties on this element from passed settings.
*
* @param {Object} params See description for more
* details or `auth-methods/oauth1.html` element that collectes configuration
* from the user.
*/
_prepareOauth: function(params) {
if (params.signatureMethod) {
var signMethod = params.signatureMethod;
if (['PLAINTEXT', 'HMAC-SHA1', 'RSA-SHA1'].indexOf(signMethod) === -1) {
throw new Error('Unsupported signature method: ' + signMethod);
}
if (signMethod === 'RSA-SHA1') {
this._privateKey = params.consumerSecret;
}
this.signatureMethod = signMethod;
}
if (params.requestTokenUrl) {
this.requestTokenUrl = params.requestTokenUrl;
}
if (params.accessTokenUrl) {
this.accessTokenUrl = params.accessTokenUrl;
}
if (params.consumerKey) {
this.consumerKey = params.consumerKey;
}
if (params.consumerSecret) {
this.consumerSecret = params.consumerSecret;
}
if (params.redirectUrl) {
this._authorizeCallback = params.redirectUrl;
}
if (params.authParamsLocation) {
this.authParamsLocation = params.authParamsLocation;
} else {
this.authParamsLocation = 'authorization';
}
if (params.authTokenMethod) {
this.authTokenMethod = params.authTokenMethod;
} else {
this.authTokenMethod = 'POST';
}
this._version = params.version || '1.0';
this._nonceSize = params.nonceSize || 32;
this._nonce = params.nonce;
this._timestamp = params.timestamp;
this._headers = params.customHeaders || this._defaultHeaders();
this._oauthParameterSeperator = ',';
},
/**
* List of default headers to send with auth request.
*
* @return {Object} Map of default headers.
*/
_defaultHeaders: function() {
return {
'Accept': '*/*',
'Connection': 'close',
'User-Agent': 'Advanced REST Client authorization'
};
},
/**
* Returns current timestamp.
*
* @return {Number} Current timestamp
*/
getTimestamp: function() {
return Math.floor((new Date()).getTime() / 1000);
},
/**
* URL encodes the string.
*
* @param {String} toEncode A string to encode.
* @return {String} Encoded string
*/
encodeData: function(toEncode) {
if (!toEncode) {
return '';
}
var result = encodeURIComponent(toEncode);
return result.replace(/\!/g, '%21')
.replace(/\'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A');
},
/**
* URL decodes data.
* Also replaces `+` with ` ` (space).
*
* @param {String} toDecode String to decode.
* @return {String} Decoded string
*/
decodeData: function(toDecode) {
if (!toDecode) {
return '';
}
toDecode = toDecode.replace(/\+/g, ' ');
return decodeURIComponent(toDecode);
},
/**
* Computes signature for the request.
*
* @param {String} signatureMethod Method to use to generate the signature.
* Supported are: `PLAINTEXT`, `HMAC-SHA1`, `RSA-SHA1`. It throws an error if
* value of this property is other than listed here.
* @param {String} requestMethod Request HTTP method.
* @param {String} url Request full URL.
* @param {Object} oauthParameters Map of oauth parameters.
* @param {?String} tokenSecret Optional, token secret.
* @return {String} Generated OAuth1 signature for given `signatureMethod`
* @param {?String} body Body used with the request. Note: this parameter
* can only be set if the request's content-type header equals
* `application/x-www-form-urlencoded`.
* @throws Error when `signatureMethod` is not one of listed here.
*/
getSignature: function(signatureMethod, requestMethod, url, oauthParameters,
tokenSecret, body) {
var signatureBase;
var key;
if (signatureMethod !== 'PLAINTEXT') {
signatureBase = this.createSignatureBase(requestMethod, url, oauthParameters, body);
}
if (signatureMethod !== 'RSA-SHA1') {
key = this.createSignatureKey(this.consumerSecret, tokenSecret);
}
switch (signatureMethod) {
case 'PLAINTEXT':
return this._createSignaturePlainText(key);
case 'RSA-SHA1':
return this._createSignatureRsaSha1(signatureBase, this._privateKey);
case 'HMAC-SHA1':
return this._createSignatureHamacSha1(signatureBase, key);
default:
throw new Error('Unknown signature method');
}
},
/**
* Normalizes URL to base string URI as described in
* https://tools.ietf.org/html/rfc5849#section-3.4.1.2
*
* @param {String} url Request full URL.
* @return {String} Base String URI
*/
_normalizeUrl: function(url) {
var parsedUrl = new URL(url);
var port = '';
if (parsedUrl.port) {
if ((parsedUrl.protocol === 'http:' && parsedUrl.port !== '80') ||
(parsedUrl.protocol === 'https:' && parsedUrl.port !== '443')) {
port = ':' + parsedUrl.port;
}
}
if (!parsedUrl.pathname || parsedUrl.pathname === '') {
parsedUrl.pathname = '/';
}
return parsedUrl.protocol + '//' + parsedUrl.hostname + port + parsedUrl.pathname;
},
/**
* @param {String} parameter Parameter name (key).
* @return {Boolean} True if the `parameter` is an OAuth 1 parameter.
*/
_isParameterNameAnOAuthParameter: function(parameter) {
return !!(parameter && parameter.indexOf('oauth_') === 0);
},
/**
* Creates an Authorization header value to trasmit OAuth params in headers
* as described in https://tools.ietf.org/html/rfc5849#section-3.5.1
*
* @param {Array} orderedParameters Oauth parameters that are already
* ordered.
* @return {String} The Authorization header value
*/
_buildAuthorizationHeaders: function(orderedParameters) {
var authHeader = 'OAuth ';
// if (this._isEcho) {
// authHeader += 'realm="' + this._realm + '",';
// }
var params = orderedParameters.map(function(item) {
if (!this._isParameterNameAnOAuthParameter(item[0])) {
return;
}
return this.encodeData(item[0]) + '="' + this.encodeData(item[1]) + '"';
}, this)
.filter(function(item) {
return !!item;
});
authHeader += params.join(this._oauthParameterSeperator + ' ');
return authHeader;
},
/**
* Creates a body for www-urlencoded content type to transmit OAuth params
* in request body as described in
* https://tools.ietf.org/html/rfc5849#section-3.5.2
*
* @param {Array} orderedParameters Oauth parameters that are already
* ordered.
* @return {String} The body to send
*/
_buildFormDataParameters: function(orderedParameters) {
return orderedParameters.map(function(item) {
if (!this._isParameterNameAnOAuthParameter(item[0])) {
return;
}
return this.encodeData(item[0]) + '=' + this.encodeData(item[1]);
}, this)
.filter(function(item) {
return !!item;
})
.join('&');
},
/**
* Adds query paramteres with OAuth 1 parameters to the URL
* as described in https://tools.ietf.org/html/rfc5849#section-3.5.3
*
* @param {Array} orderedParameters Oauth parameters that are already
* ordered.
* @return {String} URL to use with the request
*/
_buildAuthorizationQueryStirng: function(url, orderedParameters) {
var urlParser = new UrlParser(url);
orderedParameters = orderedParameters.map(function(item) {
return [
encodeURIComponent(item[0]),
encodeURIComponent(item[1])
];
});
var params = urlParser.searchParams.concat(orderedParameters);
urlParser.searchParams = params;
url = urlParser.toString();
return url;
},
// Takes an object literal that represents the arguments, and returns an array
// of argument/value pairs.
_makeArrayOfArgumentsHash: function(argumentsHash) {
var argumentPairs = [];
Object.keys(argumentsHash).forEach(function(key) {
var value = argumentsHash[key];
if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) {
argumentPairs[argumentPairs.length] = [key, value[i]];
}
} else {
argumentPairs[argumentPairs.length] = [key, value];
}
});
return argumentPairs;
},
// Sorts the encoded key value pairs by encoded name, then encoded value
_sortRequestParams: function(argumentPairs) {
// Sort by name, then value.
argumentPairs.sort(function(a, b) {
if (a[0] === b[0]) {
return a[1] < b[1] ? -1 : 1;
} else {
return a[0] < b[0] ? -1 : 1;
}
});
return argumentPairs;
},
/**
* Sort function to sort parameters as described in
* https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
*/
_sortParamsFunction: function(a, b) {
if (a[0] === b[0]) {
return String(a[1]).localeCompare(String(b[1]));
}
return String(a[0]).localeCompare(String(b[0]));
},
/**
* Normalizes request parameters as described in
* https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
*
* @param {Array} args List of parameters to normalize. It must contain
* a list of array items where first element of the array is parameter name
* and second is parameter value.
* @return {String} Normalized parameters to string.
*/
_normaliseRequestParams: function(args) {
var len = args.length;
var i = 0;
// First encode them #3.4.1.3.2 .1
for (; i < len; i++) {
args[i][0] = this.encodeData(args[i][0]);
args[i][1] = this.encodeData(args[i][1]);
}
// Then sort them #3.4.1.3.2 .2
args.sort(this._sortParamsFunction);
// Then concatenate together #3.4.1.3.2 .3 & .4
var result = args.map(function(pair) {
if (pair[0] === 'oauth_signature') {
return;
}
return pair[0] + '=' + String(pair[1]);
})
.filter(function(item) {
return !!item;
})
.join('&');
return result;
},
/**
* Computes array of parameters from the request URL.
*
* @param {String} url Full request URL
* @return {Array} Array of parameters where each item is an array with
* first element as a name of the parameter and second element as a value.
*/
_listQueryParameters: function(url) {
var parsedUrl = new UrlParser(url);
return parsedUrl.searchParams.map(function(item) {
return [
this.decodeData(item[0]),
this.decodeData(item[1])
];
}, this);
},
/**
* Computes array of parameters from the entity body.
* The body must be `application/x-www-form-urlencoded`.
*
* @param {String} body Entity body of `application/x-www-form-urlencoded`
* request
* @return {Array} Array of parameters where each item is an array with
* first element as a name of the parameter and second element as a value.
* Keys and values are percent decoded. Additionally each `+` is replaced
* with space character.
*/
_formUrlEncodedToParams: function(body) {
if (!body) {
return [];
}
var parts = body.split('&').map(function(part) {
var pair = part.split('=');
var key = this.decodeData(pair[0]);
var value = '';
if (pair[1]) {
value = this.decodeData(pair[1]);
}
return [key, value];
}, this);
return parts;
},
/**
* Creates a signature base as defined in
* https://tools.ietf.org/html/rfc5849#section-3.4.1
*
* @param {String} method HTTP method used with the request
* @param {String} url Full URL of the request
* @param {Object} oauthParams Key - value pairs of OAuth parameters
* @param {?String} body Body used with the request. Note: this parameter
* can only be set if the request's content-type header equals
* `application/x-www-form-urlencoded`.
* @return {String} A base string to be used to generate signature.
*/
createSignatureBase: function(method, url, oauthParams, body) {
var allParameter = [];
var uriParameters = this._listQueryParameters(url);
oauthParams = this._makeArrayOfArgumentsHash(oauthParams);
allParameter = uriParameters.concat(oauthParams);
if (body) {
body = this._formUrlEncodedToParams(body);
allParameter = allParameter.concat(body);
}
allParameter = this._normaliseRequestParams(allParameter);
allParameter = this.encodeData(allParameter);
url = this.encodeData(this._normalizeUrl(url));
return [method.toUpperCase(), url, allParameter].join('&');
},
/**
* Creates a signature key to compute the signature as described in
* https://tools.ietf.org/html/rfc5849#section-3.4.2
*
* @param {String} clientSecret Client secret (consumer secret).
* @param {?String} tokenSecret Optional, token secret
* @return {String} A key to be used to generate the signature.
*/
createSignatureKey: function(clientSecret, tokenSecret) {
if (!tokenSecret) {
tokenSecret = '';
} else {
tokenSecret = this.encodeData(tokenSecret);
}
clientSecret = this.encodeData(clientSecret);
return clientSecret + '&' + tokenSecret;
},
/**
* Found at http://jsfiddle.net/ARTsinn/6XaUL/
*
* @param {String} h Hexadecimal input
* @return {String} Result of transforming value to string.
*/
hex2b64: function(h) {
var b64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var b64pad = '=';
var i;
var c;
var ret = '';
for (i = 0; i + 3 <= h.length; i += 3) {
c = parseInt(h.substring(i, i + 3), 16);
ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63);
}
if (i + 1 === h.length) {
c = parseInt(h.substring(i, i + 1), 16);
ret += b64map.charAt(c << 2);
} else if (i + 2 === h.length) {
c = parseInt(h.substring(i, i + 2), 16);
ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4);
}
while ((ret.length & 3) > 0) {
ret += b64pad;
}
return ret;
},
/**
* Creates a signature for the PLAINTEXT method.
*
* In this case the signature is the key.
*
* @param {String} baseText Computed signature base text.
* @param {String} key Computed signature key.
* @return {String} Computed OAuth1 signature.
*/
_createSignaturePlainText: function(key) {
return key;
},
/**
* Creates a signature for the RSA-SHA1 method.
*
* @param {String} baseText Computed signature base text.
* @param {String} privateKey Client private key.
* @return {String} Computed OAuth1 signature.
*/
_createSignatureRsaSha1: function(baseText, privateKey) {
var rsa = new RSAKey();
rsa.readPrivateKeyFromPEMString(privateKey);
var hSig = rsa.sign(baseText, 'sha1');
return this.hex2b64(hSig);
},
/**
* Creates a signature for the HMAC-SHA1 method.
*
* @param {String} signatureBase Computed signature base text.
* @param {String} key Computed signature key.
* @return {String} Computed OAuth1 signature.
*/
_createSignatureHamacSha1: function(baseText, key) {
var hash = CryptoJS.HmacSHA1(baseText, key);
return hash.toString(CryptoJS.enc.Base64);
},
get nonceChars() {
return [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9'
];
},
_getNonce: function(nonceSize) {
var result = [];
var chars = this.nonceChars;
var charPos;
var nonceCharsLength = chars.length;
for (var i = 0; i < nonceSize; i++) {
charPos = Math.floor(Math.random() * nonceCharsLength);
result[i] = chars[charPos];
}
return result.join('');
},
_prepareParameters: function(token, tokenSecret, method, url, extraParams, body) {
var oauthParameters = {
'oauth_timestamp': this._timestamp || this.getTimestamp(),
'oauth_nonce': this._nonce || this._getNonce(this._nonceSize),
'oauth_version': this._version,
'oauth_signature_method': this.signatureMethod,
'oauth_consumer_key': this.consumerKey
};
if (token) {
oauthParameters.oauth_token = token;
}
var sig;
if (this._isEcho) {
sig = this.getSignature(this.signatureMethod, 'GET',
this._verifyCredentials, oauthParameters, tokenSecret, body);
} else {
if (extraParams) {
Object.keys(extraParams).forEach(function(key) {
oauthParameters[key] = extraParams[key];
});
}
sig = this.getSignature(this.signatureMethod, method, url, oauthParameters,
tokenSecret, body);
}
var orderedParameters = this._sortRequestParams(
this._makeArrayOfArgumentsHash(oauthParameters));
orderedParameters[orderedParameters.length] = ['oauth_signature', sig];
return orderedParameters;
},
// Encodes parameters in the map.
encodeUriParams: function(params) {
var result = Object.keys(params).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
});
return result.join('&');
},
/**
* Creates OAuth1 signature for a `request` object.
* The request object must contain:
* - `url` - String
* - `method` - String
* - `headers` - String
* It also may contain the `body` property.
*
* It alters the request object by applying OAuth1 parameters to a set
* location (qurey parameters, authorization header, body). This is
* controlled by `this.authParamsLocation` property. By default the
* parameters are applied to authorization header.
*
* @param {Object} request ARC request object.
* @param {?String} token OAuth token to use to generate the signature.
* If not set, then it will use a value from `this.lastIssuedToken`.
* @param {?String} tokenSecret OAuth token secret to use to generate the
* signature. If not set, then it will use a value from
* `this.lastIssuedToken`.
* @return {Object} The same object with applied OAuth 1 parameters.
*/
signRequestObject: function(request, token, tokenSecret) {
if (!request || !request.method || !request.url) {
return request;
}
token = token || this.lastIssuedToken.oauth_token;
tokenSecret = tokenSecret || this.lastIssuedToken.oauth_token_secret;
var method = request.method || 'GET';
method = method.toUpperCase();
var withPayload = ['GET', 'HEAD'].indexOf(request.method) === -1;
var body;
if (withPayload && request.headers && request.body) {
var contentType = this.getContentType(request.headers);
if (contentType && contentType.indexOf('application/x-www-form-urlencoded') === 0) {
body = request.body;
}
}
var orderedParameters = this._prepareParameters(token, tokenSecret, method,
request.url, {}, body);
if (this.authParamsLocation === 'authorization') {
var authorization = this._buildAuthorizationHeaders(orderedParameters);
request.headers = this._replaceHeaderValue(request.headers, 'authorization', authorization);
} else {
request.url = this._buildAuthorizationQueryStirng(request.url, orderedParameters);
}
this.clearRequestVariables();
return request;
},
_performRequest: function(token, tokenSecret, method, url, extraParams, body, contentType) {
var withPayload = ['POST', 'PUT'].indexOf(method) !== -1;
var orderedParameters = this._prepareParameters(token, tokenSecret, method, url, extraParams);
if (withPayload && !contentType) {
contentType = 'application/x-www-form-urlencoded';
}
var headers = {};
if (this.authParamsLocation === 'authorization') {
var authorization = this._buildAuthorizationHeaders(orderedParameters);
if (this._isEcho) {
headers['X-Verify-Credentials-Authorization'] = authorization;
} else {
headers.authorization = authorization;
}
} else {
url = this._buildAuthorizationQueryStirng(url, orderedParameters);
}
if (this._headers) {
Object.keys(this._headers).forEach(function(key) {
headers[key] = this._headers[key];
}, this);
}
if (extraParams) {
Object.keys(extraParams).forEach(function(key) {
if (this._isParameterNameAnOAuthParameter(key)) {
delete extraParams[key];
}
}, this);
}
if (withPayload && (extraParams && !body) && ['POST', 'PUT'].indexOf(method) !== -1) {
body = this.encodeUriParams(extraParams)
.replace(/\!/g, '%21')
.replace(/\'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A');
}
if (withPayload && !body) {
headers['Content-length'] = '0';
}
var init = {
method: method,
headers: headers
};
if (withPayload && body) {
init.body = body;
}
var responseHeaders;
return this.request(url, init)
.then(function(response) {
if (!response.ok) {
throw new Error('Token request error ended with status ' + response.status);
}
responseHeaders = response.headers;
return response.text();
})
.then(function(text) {
return {
response: text,
headers: responseHeaders
};
});
},
/**
* Exchanges temporary authorization token for authorized token.
* When ready this function fires `oauth1-token-response`
*/
getOAuthAccessToken: function(token, secret, verifier) {
var extraParams = {};
if (verifier) {
extraParams.oauth_verifier = verifier;
}
var method = this.authTokenMethod;
return this._performRequest(token, secret, method, this.accessTokenUrl, extraParams)
.then(function(response) {
if (!response.response) {
var message = 'Couldn\'t exchange token. ';
message += 'Authorization server may be down or CORS is disabled.';
throw new Error(message);
}
var params = {};
this._formUrlEncodedToParams(response.response).map(function(pair) {
params[pair[0]] = pair[1];
});
return params;
}.bind(this))
.then(function(tokenInfo) {
this.clearRequestVariables();
this.lastIssuedToken = tokenInfo;
this.fire('oauth1-token-response', tokenInfo);
}.bind(this));
},
/**
* Clears variables set for current request after signature has been
* generated and token obtained.
*/
clearRequestVariables: function() {
this.temporaryCredentials = undefined;
this._timestamp = undefined;
this._nonce = undefined;
},
/**
* Requests the authorization server for temporarty authorization token.
* This token should be passed to `authorizationUri` as a `oauth_token`
* parameter.
*
* @param {Object} extraParams List of extra parameters to include in the
* request.
* @return {Promise} A promise resolved to a map of OAuth 1 parameters:
* `oauth_token`, `oauth_token_secret`, `oauth_verifier` and
* `oauth_callback_confirmed` (for 1.0a version).
*/
getOAuthRequestToken: function(extraParams) {
extraParams = extraParams || {};
if (this._authorizeCallback) {
extraParams.oauth_callback = this._authorizeCallback;
}
var method = this.authTokenMethod;
return this._performRequest(null, null, method, this.requestTokenUrl, extraParams)
.then(function(response) {
if (!response.response) {
var message = 'Couldn\'t request for authorization token. ';
message += 'Authorization server may be down or CORS is disabled.';
throw new Error(message);
}
var params = {};
this._formUrlEncodedToParams(response.response).map(function(pair) {
params[pair[0]] = pair[1];
});
return params;
}.bind(this));
},
/**
* Makes a HTTP request.
* Before making the request it sends `auth-request-proxy` custom event
* with the URL and init object in event's detail object.
* If the event is cancelled then it will use detail's `result` value to
* return from this function. The `result` must be a Promise that will
* resolve to a `Response` object.
* Otherwise it will use internall `fetch` implementation.
*
* @param {String} url An URL to call
* @param {Object} init Init object that will be passed to a `Request`
* object.
* @return {Promise} A promise that resolves to a `Response` object.
*/
request: function(url, init) {
var event = this.fire('auth-request-proxy', {
url: init,
init: init
}, {
cancelable: true,
composed: true
});
var p;
if (event.defaultPrevented) {
p = event.detail.result;
} else {
p = this._fetch(url, init);
}
return p;
},
/**
* Performs a HTTP request.
* If `proxy` is set or `iron-meta` with a key `auth-proxy` is set then
* it will prefix the URL with the value of proxy.
*
* @param {String} url An URL to call
* @param {Object} init Init object that will be passed to a `Request`
* object.
* @return {Promise} A promise that resolves to a `Response` object.
*/
_fetch: function(url, init) {
var proxy;
if (this.proxy) {
proxy = this.proxy;
} else {
proxy = document.createElement('iron-meta').byKey('auth-proxy');
}
if (proxy) {
url = proxy + url;
}
init.mode = 'cors';
return fetch(url, init);
},
_listenPopup: function(e) {
if (!location || !e.source || !this._popup || e.origin !== location.origin ||
e.source.location.href !== this._popup.location.href) {
return;
}
var tokenInfo = e.data;
this.popupClosedProperly = true;
switch (this._next) {
case 'exchange-token':
this.getOAuthAccessToken(tokenInfo.oauthToken,
this.temporaryCredentials.oauth_token_secret,
tokenInfo.oauthVerifier);
break;
}
this._popup.close();
},
// Observer if the popup has been closed befor the data has been received.
_observePopupState: function() {
var context = this;
var popupCheckInterval = setInterval(function() {
if (!context._popup || context._popup.closed) {
clearInterval(popupCheckInterval);
context._beforePopupUnloadHandler();
}
}, 500);
},
_beforePopupUnloadHandler: function() {
if (this.popupClosedProperly) {
return;
}
this._popup = undefined;
this.fire('oauth1-error', {
'message': 'No response has been recorded.',
'code': 'no-response'
});
},
/**
* Adds camel case keys to a map of parameters.
* It adds new keys to the object tranformed from `oauth_token`
* to `oauthToken`
*/
parseMapKeys: function(obj) {
Object.keys(obj).forEach(function(key) {
obj = this._parseParameter(key, obj);
}, this);
return obj;
},
_parseParameter: function(param, settings) {
if (!(param in settings)) {
return settings;
}
var value = settings[param];
var oauthParam;
if (this._caseMap[param]) {
oauthParam = this._caseMap[param];
} else {
oauthParam = this._getCaseParam(param);
}
settings[oauthParam] = value;
return settings;
},
_getCaseParam: function(param) {
return 'oauth_' + param.replace(this._camelRegex, '_$1').toLowerCase();
},
/**
* Get the Content-Type value from the headers.
*
* @param {String} headers HTTP headers string
* @return {String} A content-type header value or null if not found
*/
getContentType: function(headers) {
if (!headers || typeof headers !== 'string') {
return;
}
headers = headers.trim();
if (headers === '') {
return;
}
var re = /^content-type:\s?(.*)$/im;
var match = headers.match(re);
if (!match) {
return;
}
var ct = match[1].trim();
if (ct.indexOf('multipart') === -1) {
var index = ct.indexOf('; ');
if (index > 0) {
ct = ct.substr(0, index);
}
}
return ct;
},
/**
* Replace value for given header in the headers list.
*
* @param {String} headers HTTP headers string
* @param {String} name Header name to be replaced.
* @param {String} value Header value to be repleced.
* @return {String} Updated headers.
*/
_replaceHeaderValue: function(headers, name, value) {
headers = this._headersStringToArray(headers);
var _name = name.toLowerCase();
var found = false;
for (var i = 0, len = headers.length; i < len; i++) {
var header = headers[i];
if (header.name.toLowerCase() === _name) {
header.value = value;
found = true;
break;
}
}
if (!found) {
headers.push({
name: name,
value: value
});
}
var result = headers.map(function(header) {
var key = header.name;
var value = header.value;
var _result;
if (key && key.trim() !== '') {
_result = key + ': ';
if (value && value.trim() !== '') {
_result += value;
}
}
return _result;
})
.filter(function(item) {
return !!item;
})
.join('\n');
return result;
},
_headersStringToArray: function(headersString) {
if (!headersString || headersString.trim() === '') {
return [];
}
if (typeof headersString !== 'string') {
return [];
}
var result = [];
var headers = headersString.split(/\n/gim);
for (var i = 0, len = headers.length; i < len; i++) {
var line = headers[i].trim();
if (line === '') {
continue;
}
var sepPosition = line.indexOf(':');
if (sepPosition === -1) {
result[result.length] = {
name: line,
value: ''
};
continue;
}
var name = line.substr(0, sepPosition);
var value = line.substr(sepPosition + 1).trim();
var obj = {
name: name,
value: value
};
result.push(obj);
}
return result;
}
/**
* Fired when authorization is unsuccessful
*
* @event oauth1-error
* @param {String} message Human readable error message
* @param {String} code Error code associated with the error. See description
* of the element fo code mening.
*/
/**
* Fired when the authorization is successful and token and secret are ready.
*
* @event oauth1-token-response
* @param {String} oauth_token Received OAuth1 token
* @param {String} oauth_token_secret Received OAuth1 token secret
*/
});
})();
</script>