@advanced-rest-client/oauth-authorization
Advanced tools
Comparing version 3.0.2 to 4.0.0
@@ -433,1 +433,42 @@ <a name="2.0.2"></a> | ||
<a name="4.0.0"></a> | ||
# [4.0.0](https://github.com/advanced-rest-client/oauth-authorization/compare/3.0.1...4.0.0) (2020-03-04) | ||
## Build | ||
* bumping version [839f35f](https://github.com/advanced-rest-client/oauth-authorization/commit/839f35f1ec3a7632a9f268f8a30f25ff600839cf) by Pawel Psztyc | ||
## Continuous integration | ||
* updating SL configuration [f4ebfee](https://github.com/advanced-rest-client/oauth-authorization/commit/f4ebfee0050e5bb861b3b36cc88744fbba93ad0e) by Pawel Psztyc | ||
* updating Travis configuration [d3a48f4](https://github.com/advanced-rest-client/oauth-authorization/commit/d3a48f4495427c12fd5a040939ed3ee2a805b7be) by Pawel Psztyc | ||
## Update | ||
* adding types [48c7017](https://github.com/advanced-rest-client/oauth-authorization/commit/48c70179c3f5b093869ec752212e31b2c276f08f) by Pawel Psztyc | ||
* adding exception handling for OAuth 1 [4f3be8d](https://github.com/advanced-rest-client/oauth-authorization/commit/4f3be8d9ca27e48120f78346e07bb5c6b1de98c6) by Pawel Psztyc | ||
* bumping major version [42a2238](https://github.com/advanced-rest-client/oauth-authorization/commit/42a2238b307e9c0c7e228ce13696a43f253189f4) by Pawel Psztyc | ||
* removing unused commands from `lint-starged` cnf [b1c8e47](https://github.com/advanced-rest-client/oauth-authorization/commit/b1c8e47bf562498d2e329e055494994903cc44c9) by Pawel Psztyc | ||
* [ci skip] automated merge master->stage. syncing main branches [32b86fd](https://github.com/advanced-rest-client/oauth-authorization/commit/32b86fde6967e653563a60bde92e5128bc77301c) by Ci agent | ||
* upgrding dependencies [838bc1e](https://github.com/advanced-rest-client/oauth-authorization/commit/838bc1e268f211aa0b91ff1c5e74f0de80f55b91) by Pawel Psztyc | ||
* upgrading dependencies [4429e94](https://github.com/advanced-rest-client/oauth-authorization/commit/4429e94c3ca1731b3c3c1c1fb47bcb34852c8f29) by Pawel | ||
## Features | ||
* adding `signRequest()` method [a682a65](https://github.com/advanced-rest-client/oauth-authorization/commit/a682a657cc4c7ae6e9c93424e4eb5bf255ad1c50) by Pawel Psztyc | ||
## Refactor | ||
* move sources to src and add events-target-mixin support [d37b847](https://github.com/advanced-rest-client/oauth-authorization/commit/d37b8478b703297584913eef89c9202fe052925a) by Pawel Psztyc | ||
## Testing | ||
* temporaily removing Safari-1 [36e502d](https://github.com/advanced-rest-client/oauth-authorization/commit/36e502df5b6dda8eeaba4f75e7f497eb382db5a9) by Pawel Psztyc | ||
* updating test commands [e0800ff](https://github.com/advanced-rest-client/oauth-authorization/commit/e0800ffab5988e67e38f3351ab0997f413d489fb) by Pawel Psztyc | ||
@@ -13,449 +13,5 @@ /** | ||
// tslint:disable:variable-name Describing an API that's defined elsewhere. | ||
// tslint:disable:no-any describes the API as best we are able today | ||
import {LitElement} from 'lit-element'; | ||
import {OAuth1Authorization} from './src/OAuth1Authorization.js'; | ||
import {HeadersParserMixin} from '@advanced-rest-client/headers-parser-mixin/headers-parser-mixin.js'; | ||
export {OAuth1Authorization}; | ||
declare class OAuth1Authorization { | ||
/** | ||
* Latest valid token exchanged with the authorization endpoint. | ||
*/ | ||
lastIssuedToken: object|null|undefined; | ||
/** | ||
* Returns a list of characters that can be used to buid nonce. | ||
*/ | ||
readonly nonceChars: Array<String|null>|null; | ||
/** | ||
* If set, requests made by this element to authorization endpoint will be | ||
* prefixed with the proxy value. | ||
*/ | ||
proxy: string|null|undefined; | ||
/** | ||
* OAuth 1 token authorization endpoint. | ||
*/ | ||
requestTokenUri: string|null|undefined; | ||
/** | ||
* Oauth 1 token exchange endpoint | ||
*/ | ||
accessTokenUri: string|null|undefined; | ||
/** | ||
* Oauth 1 consumer key to use with auth request | ||
*/ | ||
consumerKey: string|null|undefined; | ||
/** | ||
* Oauth 1 consumer secret to be used to generate the signature. | ||
*/ | ||
consumerSecret: string|null|undefined; | ||
/** | ||
* A signature generation method. | ||
* Once of: `PLAINTEXT`, `HMAC-SHA1` or `RSA-SHA1` | ||
*/ | ||
signatureMethod: string|null|undefined; | ||
/** | ||
* 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: string|null|undefined; | ||
_caseMap: object|null|undefined; | ||
_camelRegex: object|null|undefined; | ||
/** | ||
* Returns `application/x-www-form-urlencoded` content type value. | ||
*/ | ||
urlEncodedType: string|null|undefined; | ||
connectedCallback(): void; | ||
disconnectedCallback(): void; | ||
/** | ||
* 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(e: CustomEvent|null): void; | ||
/** | ||
* 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 request ARC request object | ||
* @param auth Token request auth object | ||
*/ | ||
_applyBeforeRequestSignature(request: object|null, auth: String|null): void; | ||
/** | ||
* 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(e: CustomEvent|null): void; | ||
/** | ||
* Performs a request to authorization server. | ||
* | ||
* @param settings Oauth1 configuration. See description for more | ||
* details or `auth-methods/oauth1.html` element that collectes configuration | ||
* from the user. | ||
*/ | ||
authorize(settings: object|null): void; | ||
/** | ||
* Sets a configuration properties on this element from passed settings. | ||
* | ||
* @param params See description for more | ||
* details or `auth-methods/oauth1.html` element that collectes configuration | ||
* from the user. | ||
*/ | ||
_prepareOauth(params: object|null): void; | ||
/** | ||
* List of default headers to send with auth request. | ||
* | ||
* @returns Map of default headers. | ||
*/ | ||
_defaultHeaders(): object|null; | ||
/** | ||
* Returns current timestamp. | ||
* | ||
* @returns Current timestamp | ||
*/ | ||
getTimestamp(): Number|null; | ||
/** | ||
* URL encodes the string. | ||
* | ||
* @param toEncode A string to encode. | ||
* @returns Encoded string | ||
*/ | ||
encodeData(toEncode: String|null): String|null; | ||
/** | ||
* Normalizes url encoded values as defined in the OAuth 1 spec. | ||
* | ||
* @param url URI encoded params. | ||
* @returns Normalized params. | ||
*/ | ||
_finishEncodeParams(url: String|null): String|null; | ||
/** | ||
* URL decodes data. | ||
* Also replaces `+` with ` ` (space). | ||
* | ||
* @param toDecode String to decode. | ||
* @returns Decoded string | ||
*/ | ||
decodeData(toDecode: String|null): String|null; | ||
/** | ||
* Computes signature for the request. | ||
* | ||
* @param 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 requestMethod Request HTTP method. | ||
* @param url Request full URL. | ||
* @param oauthParameters Map of oauth parameters. | ||
* @param tokenSecret Optional, token secret. | ||
* @param 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`. | ||
* @returns Generated OAuth1 signature for given `signatureMethod` | ||
*/ | ||
getSignature(signatureMethod: String|null, requestMethod: String|null, url: String|null, oauthParameters: object|null, tokenSecret: String|null, body: String|null): String|null; | ||
/** | ||
* Normalizes URL to base string URI as described in | ||
* https://tools.ietf.org/html/rfc5849#section-3.4.1.2 | ||
* | ||
* @param url Request full URL. | ||
* @returns Base String URI | ||
*/ | ||
_normalizeUrl(url: String|null): String|null; | ||
/** | ||
* @param parameter Parameter name (key). | ||
* @returns True if the `parameter` is an OAuth 1 parameter. | ||
*/ | ||
_isParameterNameAnOAuthParameter(parameter: String|null): Boolean|null; | ||
/** | ||
* 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 orderedParameters Oauth parameters that are already | ||
* ordered. | ||
* @returns The Authorization header value | ||
*/ | ||
_buildAuthorizationHeaders(orderedParameters: any[]|null): String|null; | ||
/** | ||
* 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 orderedParameters Oauth parameters that are already | ||
* ordered. | ||
* @returns The body to send | ||
*/ | ||
_buildFormDataParameters(orderedParameters: any[]|null): String|null; | ||
/** | ||
* Adds query paramteres with OAuth 1 parameters to the URL | ||
* as described in https://tools.ietf.org/html/rfc5849#section-3.5.3 | ||
* | ||
* @param orderedParameters Oauth parameters that are already | ||
* ordered. | ||
* @returns URL to use with the request | ||
*/ | ||
_buildAuthorizationQueryStirng(url: String|null, orderedParameters: any[]|null): String|null; | ||
/** | ||
* of argument/value pairs. | ||
*/ | ||
_makeArrayOfArgumentsHash(argumentsHash: any): any; | ||
/** | ||
* Sorts the encoded key value pairs by encoded name, then encoded value | ||
*/ | ||
_sortRequestParams(argumentPairs: any): any; | ||
/** | ||
* Sort function to sort parameters as described in | ||
* https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 | ||
*/ | ||
_sortParamsFunction(a: String|null, b: String|null): Number|null; | ||
/** | ||
* Normalizes request parameters as described in | ||
* https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 | ||
* | ||
* @param 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. | ||
* @returns Normalized parameters to string. | ||
*/ | ||
_normaliseRequestParams(args: any[]|null): String|null; | ||
/** | ||
* Computes array of parameters from the request URL. | ||
* | ||
* @param url Full request URL | ||
* @returns 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(url: String|null): any[]|null; | ||
/** | ||
* Computes array of parameters from the entity body. | ||
* The body must be `application/x-www-form-urlencoded`. | ||
* | ||
* @param body Entity body of `application/x-www-form-urlencoded` | ||
* request | ||
* @returns 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(body: String|null): any[]|null; | ||
/** | ||
* Creates a signature base as defined in | ||
* https://tools.ietf.org/html/rfc5849#section-3.4.1 | ||
* | ||
* @param method HTTP method used with the request | ||
* @param url Full URL of the request | ||
* @param oauthParams Key - value pairs of OAuth parameters | ||
* @param 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`. | ||
* @returns A base string to be used to generate signature. | ||
*/ | ||
createSignatureBase(method: String|null, url: String|null, oauthParams: object|null, body: String|null): String|null; | ||
/** | ||
* Creates a signature key to compute the signature as described in | ||
* https://tools.ietf.org/html/rfc5849#section-3.4.2 | ||
* | ||
* @param clientSecret Client secret (consumer secret). | ||
* @param tokenSecret Optional, token secret | ||
* @returns A key to be used to generate the signature. | ||
*/ | ||
createSignatureKey(clientSecret: String|null, tokenSecret: String|null): String|null; | ||
/** | ||
* Found at http://jsfiddle.net/ARTsinn/6XaUL/ | ||
* | ||
* @param h Hexadecimal input | ||
* @returns Result of transforming value to string. | ||
*/ | ||
hex2b64(h: String|null): String|null; | ||
/** | ||
* Creates a signature for the PLAINTEXT method. | ||
* | ||
* In this case the signature is the key. | ||
* | ||
* @param key Computed signature key. | ||
* @returns Computed OAuth1 signature. | ||
*/ | ||
_createSignaturePlainText(key: String|null): String|null; | ||
/** | ||
* Creates a signature for the RSA-SHA1 method. | ||
* | ||
* @param baseText Computed signature base text. | ||
* @param privateKey Client private key. | ||
* @returns Computed OAuth1 signature. | ||
*/ | ||
_createSignatureRsaSha1(baseText: String|null, privateKey: String|null): String|null; | ||
/** | ||
* Creates a signature for the HMAC-SHA1 method. | ||
* | ||
* @param baseText Computed signature base text. | ||
* @param key Computed signature key. | ||
* @returns Computed OAuth1 signature. | ||
*/ | ||
_createSignatureHamacSha1(baseText: String|null, key: String|null): String|null; | ||
_getNonce(nonceSize: any): any; | ||
_prepareParameters(token: any, tokenSecret: any, method: any, url: any, extraParams: any, body: any): any; | ||
/** | ||
* Encodes parameters in the map. | ||
*/ | ||
encodeUriParams(params: any): any; | ||
/** | ||
* 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 request ARC request object. | ||
* @param token OAuth token to use to generate the signature. | ||
* If not set, then it will use a value from `this.lastIssuedToken`. | ||
* @param tokenSecret OAuth token secret to use to generate the | ||
* signature. If not set, then it will use a value from | ||
* `this.lastIssuedToken`. | ||
* @returns The same object with applied OAuth 1 parameters. | ||
*/ | ||
signRequestObject(request: object|null, token: String|null, tokenSecret: String|null): object|null; | ||
_performRequest(token: any, tokenSecret: any, method: any, url: any, extraParams: any, body: any, contentType: any): any; | ||
/** | ||
* Exchanges temporary authorization token for authorized token. | ||
* When ready this function fires `oauth1-token-response` | ||
*/ | ||
getOAuthAccessToken(token: String|null, secret: String|null, verifier: String|null): Promise<any>|null; | ||
/** | ||
* Clears variables set for current request after signature has been | ||
* generated and token obtained. | ||
*/ | ||
clearRequestVariables(): void; | ||
/** | ||
* Requests the authorization server for temporarty authorization token. | ||
* This token should be passed to `authorizationUri` as a `oauth_token` | ||
* parameter. | ||
* | ||
* @param extraParams List of extra parameters to include in the | ||
* request. | ||
* @returns 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(extraParams: object|null): Promise<any>|null; | ||
/** | ||
* 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 url An URL to call | ||
* @param init Init object that will be passed to a `Request` | ||
* object. | ||
* @returns A promise that resolves to a `Response` object. | ||
*/ | ||
request(url: String|null, init: object|null): Promise<any>|null; | ||
/** | ||
* 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 url An URL to call | ||
* @param init Init object that will be passed to a `Request` | ||
* object. | ||
* @returns A promise that resolves to a `Response` object. | ||
*/ | ||
_fetch(url: String|null, init: object|null): Promise<any>|null; | ||
_listenPopup(e: any): void; | ||
/** | ||
* Observer if the popup has been closed befor the data has been received. | ||
*/ | ||
_observePopupState(): void; | ||
_beforePopupUnloadHandler(): void; | ||
/** | ||
* Dispatches an error event that propagates through the DOM. | ||
*/ | ||
_dispatchError(message: String|null, code: String|null): void; | ||
/** | ||
* Adds camel case keys to a map of parameters. | ||
* It adds new keys to the object tranformed from `oauth_token` | ||
* to `oauthToken` | ||
*/ | ||
parseMapKeys(obj: object|null): object|null; | ||
/** | ||
* Parses a query parameter object to produce camel case map of parameters. | ||
* This sets values to the `settings` object which is passed by reference. | ||
* No need to return value. | ||
* | ||
* @param param Key in the `settings` object. | ||
* @param settings Parameters. | ||
*/ | ||
_parseParameter(param: String|null, settings: object|null): object|null; | ||
_getCaseParam(param: any): any; | ||
} | ||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"oauth1-authorization": OAuth1Authorization; | ||
} | ||
} |
@@ -14,1290 +14,4 @@ /** | ||
*/ | ||
import { LitElement } from 'lit-element'; | ||
import '@polymer/iron-meta/iron-meta.js'; | ||
import { HeadersParserMixin } from '@advanced-rest-client/headers-parser-mixin/headers-parser-mixin.js'; | ||
function noop() {} | ||
/** | ||
* @typedef AuthSettings | ||
* @property {Boolean} valid | ||
* @property {String} type | ||
* @property {Object} settings | ||
*/ | ||
/** | ||
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` | | ||
| `requestTokenUri` | `String` | Token request URI. Optional for before request. Required for authorization | | ||
| `accessTokenUri` | `String` | Access token request URI. Optional for before request. Required for authorization | | ||
| `authorizationUri` | `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. | | ||
| `redirectUri` | `String` | Redirect URI for the authorization. Optional for before request. | | ||
| `authParamsLocation` | `String` | Location of the authorization parameters. Default to `authorization` header | | ||
| `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. | ||
## Required dependencies | ||
The `CryptoJS` and `RSAKey` libraries are not included into the element sources. | ||
If your project do not use this libraries already include it into your project. | ||
This component also uses `URLSearchParams` so provide a polyfill for `URL` and `URLSearchParams`. | ||
``` | ||
npm i cryptojslib jsrsasign | ||
``` | ||
```html | ||
<script src="../cryptojslib/components/core.js"></script> | ||
<script src="../cryptojslib/rollups/sha1.js"></script> | ||
<script src="../cryptojslib/components/enc-base64-min.js"></script> | ||
<script src="../cryptojslib/rollups/md5.js"></script> | ||
<script src="../cryptojslib/rollups/hmac-sha1.js"></script> | ||
<script src="../jsrsasign/lib/jsrsasign-rsa-min.js"></script> | ||
``` | ||
@customElement | ||
@memberof LogicElements | ||
@appliesMixin HeadersParserMixin | ||
*/ | ||
if (window) { | ||
window.forceJURL = true; | ||
} | ||
export class OAuth1Authorization extends HeadersParserMixin(LitElement) { | ||
get lastIssuedToken() { | ||
return this._lastIssuedToken; | ||
} | ||
set lastIssuedToken(value) { | ||
const old = this._lastIssuedToken; | ||
if (old === value) { | ||
return; | ||
} | ||
this._lastIssuedToken = value; | ||
this.dispatchEvent( | ||
new CustomEvent('last-issued-token-changed', { | ||
detail: { | ||
value | ||
} | ||
}) | ||
); | ||
} | ||
static get properties() { | ||
return { | ||
/** | ||
* If set, requests made by this element to authorization endpoint will be | ||
* prefixed with the proxy value. | ||
*/ | ||
proxy: { type: String }, | ||
/** | ||
* Latest valid token exchanged with the authorization endpoint. | ||
*/ | ||
lastIssuedToken: { type: Object }, | ||
/** | ||
* OAuth 1 token authorization endpoint. | ||
*/ | ||
requestTokenUri: { type: String }, | ||
/** | ||
* Oauth 1 token exchange endpoint | ||
*/ | ||
accessTokenUri: { type: String }, | ||
/** | ||
* Oauth 1 consumer key to use with auth request | ||
*/ | ||
consumerKey: { type: String }, | ||
/** | ||
* Oauth 1 consumer secret to be used to generate the signature. | ||
*/ | ||
consumerSecret: { type: String }, | ||
/** | ||
* A signature generation method. | ||
* Once of: `PLAINTEXT`, `HMAC-SHA1` or `RSA-SHA1` | ||
*/ | ||
signatureMethod: { type: String }, | ||
/** | ||
* 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 }, | ||
_caseMap: { type: Object }, | ||
_camelRegex: { type: Object }, | ||
/** | ||
* Returns `application/x-www-form-urlencoded` content type value. | ||
*/ | ||
urlEncodedType: { type: String } | ||
}; | ||
} | ||
constructor() { | ||
super(); | ||
this._tokenRequestedHandler = this._tokenRequestedHandler.bind(this); | ||
this._listenPopup = this._listenPopup.bind(this); | ||
this._handleRequest = this._handleRequest.bind(this); | ||
this.signatureMethod = 'HMAC-SHA1'; | ||
this.authParamsLocation = 'authorization'; | ||
this._caseMap = {}; | ||
this._camelRegex = /([A-Z])/g; | ||
this.urlEncodedType = 'application/x-www-form-urlencoded'; | ||
} | ||
connectedCallback() { | ||
super.connectedCallback(); | ||
window.addEventListener('oauth1-token-requested', this._tokenRequestedHandler); | ||
window.addEventListener('message', this._listenPopup); | ||
window.addEventListener('before-request', this._handleRequest); | ||
this.setAttribute('aria-hidden', 'true'); | ||
} | ||
disconnectedCallback() { | ||
super.disconnectedCallback(); | ||
window.removeEventListener('oauth1-token-requested', this._tokenRequestedHandler); | ||
window.removeEventListener('message', this._listenPopup); | ||
window.removeEventListener('before-request', this._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. | ||
* @param {CustomEvent} e | ||
*/ | ||
_handleRequest(e) { | ||
const request = e.detail; | ||
if (!request.auth || request.auth.type !== 'oauth1') { | ||
return; | ||
} | ||
this._applyBeforeRequestSignature(request, request.auth); | ||
} | ||
/** | ||
* This is similar to `signRequestObject()` fut it accepts the request object | ||
* and authorization settings separately and it uses OAuth configuration | ||
* from the auth object. | ||
* | ||
* @param {Object} request ARC/API Console request object | ||
* @param {AuthSettings|Array<AuthSettings>} auth Authorization object | ||
* @return {Object} Signed request object. | ||
*/ | ||
signRequest(request, auth) { | ||
if (!auth) { | ||
return request; | ||
} | ||
const authInfo = Array.isArray(auth) | ||
? auth.find((item) => item.type === 'oauth 1') | ||
: auth.type === 'oauth1' || auth.type === 'oauth 1' | ||
? auth | ||
: undefined; | ||
if (!authInfo) { | ||
return request; | ||
} | ||
const authSettings = authInfo.settings || {}; | ||
const { token, tokenSecret } = authSettings; | ||
if (!token || !tokenSecret) { | ||
return request; | ||
} | ||
this._applyBeforeRequestSignature(request, authSettings); | ||
return request; | ||
} | ||
/** | ||
* 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} auth Token request auth object | ||
*/ | ||
_applyBeforeRequestSignature(request, auth) { | ||
if (!request || !request.method || !request.url) { | ||
return; | ||
} | ||
try { | ||
this._prepareOauth(auth); | ||
} catch (_) { | ||
return; | ||
} | ||
const token = auth.token || this.lastIssuedToken.oauth_token; | ||
const tokenSecret = auth.tokenSecret || this.lastIssuedToken.oauth_token_secret; | ||
let method = request.method || 'GET'; | ||
method = method.toUpperCase(); | ||
const withPayload = ['GET', 'HEAD'].indexOf(request.method) === -1; | ||
let body; | ||
if (withPayload && request.headers && request.body) { | ||
let contentType; | ||
try { | ||
contentType = this.getContentType(request.headers); | ||
} catch (e) { | ||
// ... | ||
} | ||
if (contentType && contentType.indexOf(this.urlEncodedType) === 0) { | ||
body = request.body; | ||
} | ||
} | ||
const orderedParameters = this._prepareParameters(token, tokenSecret, method, request.url, {}, body); | ||
if (this.authParamsLocation === 'authorization') { | ||
const authorization = this._buildAuthorizationHeaders(orderedParameters); | ||
try { | ||
request.headers = this.replaceHeaderValue(request.headers, 'authorization', authorization); | ||
} catch (_) { | ||
noop(); | ||
} | ||
} 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. | ||
* | ||
* @param {CustomEvent} e | ||
*/ | ||
_tokenRequestedHandler(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(settings) { | ||
try { | ||
this._prepareOauth(settings); | ||
} catch (e) { | ||
noop(); | ||
this._dispatchError('Unable to authorize: ' + e.message, 'params-error'); | ||
return; | ||
} | ||
this.getOAuthRequestToken() | ||
.then((temporaryCredentials) => { | ||
this.temporaryCredentials = temporaryCredentials; | ||
const authorizationUri = settings.authorizationUri + '?oauth_token=' + temporaryCredentials.oauth_token; | ||
this.popupClosedProperly = undefined; | ||
this._popup = window.open(authorizationUri, 'api-console-oauth1'); | ||
if (!this._popup) { | ||
// popup blocked. | ||
this._dispatchError('Authorization popup is blocked', 'popup-blocked'); | ||
return; | ||
} | ||
this._next = 'exchange-token'; | ||
this._popup.window.focus(); | ||
this._observePopupState(); | ||
}) | ||
.catch((e) => { | ||
const msg = e.message || 'Unknown error when getting the token'; | ||
this._dispatchError(msg, 'token-request-error'); | ||
}); | ||
} | ||
/** | ||
* 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(params) { | ||
if (params.signatureMethod) { | ||
const 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.requestTokenUri) { | ||
this.requestTokenUri = params.requestTokenUri; | ||
} | ||
if (params.accessTokenUri) { | ||
this.accessTokenUri = params.accessTokenUri; | ||
} | ||
if (params.consumerKey) { | ||
this.consumerKey = params.consumerKey; | ||
} | ||
if (params.consumerSecret) { | ||
this.consumerSecret = params.consumerSecret; | ||
} | ||
if (params.redirectUri) { | ||
this._authorizeCallback = params.redirectUri; | ||
} | ||
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() { | ||
return { | ||
'Accept': '*/*', | ||
'Connection': 'close', | ||
'User-Agent': 'Advanced REST Client authorization' | ||
}; | ||
} | ||
/** | ||
* Returns current timestamp. | ||
* | ||
* @return {Number} Current timestamp | ||
*/ | ||
getTimestamp() { | ||
return Math.floor(new Date().getTime() / 1000); | ||
} | ||
/** | ||
* URL encodes the string. | ||
* | ||
* @param {String} toEncode A string to encode. | ||
* @return {String} Encoded string | ||
*/ | ||
encodeData(toEncode) { | ||
if (!toEncode) { | ||
return ''; | ||
} | ||
const result = encodeURIComponent(toEncode); | ||
return this._finishEncodeParams(result); | ||
} | ||
/** | ||
* Normalizes url encoded values as defined in the OAuth 1 spec. | ||
* | ||
* @param {String} url URI encoded params. | ||
* @return {String} Normalized params. | ||
*/ | ||
_finishEncodeParams(url) { | ||
return url | ||
.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(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(signatureMethod, requestMethod, url, oauthParameters, tokenSecret, body) { | ||
let signatureBase; | ||
let 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(url) { | ||
const parsedUrl = new URL(url); | ||
let 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(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(orderedParameters) { | ||
let authHeader = 'OAuth '; | ||
const params = []; | ||
orderedParameters.forEach((item) => { | ||
if (!this._isParameterNameAnOAuthParameter(item[0])) { | ||
return; | ||
} | ||
params.push(this.encodeData(item[0]) + '="' + this.encodeData(item[1]) + '"'); | ||
}); | ||
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(orderedParameters) { | ||
const result = []; | ||
orderedParameters.forEach((item) => { | ||
if (!this._isParameterNameAnOAuthParameter(item[0])) { | ||
return; | ||
} | ||
result.push(this.encodeData(item[0]) + '=' + this.encodeData(item[1])); | ||
}); | ||
return result.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 {String} url | ||
* @param {Array} orderedParameters Oauth parameters that are already | ||
* ordered. | ||
* @return {String} URL to use with the request | ||
*/ | ||
_buildAuthorizationQueryStirng(url, orderedParameters) { | ||
const parser = new URL(url); | ||
orderedParameters.forEach((item) => { | ||
parser.searchParams.append(item[0], item[1]); | ||
}); | ||
return parser.toString(); | ||
} | ||
// Takes an object literal that represents the arguments, and returns an array | ||
// of argument/value pairs. | ||
_makeArrayOfArgumentsHash(argumentsHash) { | ||
const argumentPairs = []; | ||
Object.keys(argumentsHash).forEach(function(key) { | ||
const value = argumentsHash[key]; | ||
if (Array.isArray(value)) { | ||
for (let i = 0, len = value.length; i < len; 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(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 | ||
* @param {String} a | ||
* @param {String} b | ||
* @return {Number} | ||
*/ | ||
_sortParamsFunction(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(args) { | ||
const len = args.length; | ||
let 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 | ||
const result = []; | ||
args.forEach((pair) => { | ||
if (pair[0] === 'oauth_signature') { | ||
return; | ||
} | ||
result.push(pair[0] + '=' + String(pair[1])); | ||
}); | ||
return result.join('&'); | ||
} | ||
/** | ||
* 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(url) { | ||
const parsedUrl = new URL(url); | ||
const result = []; | ||
parsedUrl.searchParams.forEach((value, key) => { | ||
result[result.length] = [this.decodeData(key), this.decodeData(value)]; | ||
}); | ||
return result; | ||
} | ||
/** | ||
* 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(body) { | ||
if (!body) { | ||
return []; | ||
} | ||
const parts = body.split('&').map((part) => { | ||
const pair = part.split('='); | ||
const key = this.decodeData(pair[0]); | ||
let value = ''; | ||
if (pair[1]) { | ||
value = this.decodeData(pair[1]); | ||
} | ||
return [key, value]; | ||
}); | ||
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(method, url, oauthParams, body) { | ||
let allParameter = []; | ||
const 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(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(h) { | ||
const b64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; | ||
const b64pad = '='; | ||
let i; | ||
let c; | ||
let 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} key Computed signature key. | ||
* @return {String} Computed OAuth1 signature. | ||
*/ | ||
_createSignaturePlainText(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(baseText, privateKey) { | ||
/* global RSAKey */ | ||
const rsa = new RSAKey(); | ||
rsa.readPrivateKeyFromPEMString(privateKey); | ||
const hSig = rsa.sign(baseText, 'sha1'); | ||
return this.hex2b64(hSig); | ||
} | ||
/** | ||
* Creates a signature for the HMAC-SHA1 method. | ||
* | ||
* @param {String} baseText Computed signature base text. | ||
* @param {String} key Computed signature key. | ||
* @return {String} Computed OAuth1 signature. | ||
*/ | ||
_createSignatureHamacSha1(baseText, key) { | ||
/* global CryptoJS */ | ||
const hash = CryptoJS.HmacSHA1(baseText, key); | ||
return hash.toString(CryptoJS.enc.Base64); | ||
} | ||
/** | ||
* Returns a list of characters that can be used to buid nonce. | ||
* | ||
* @return {Array<String>} | ||
*/ | ||
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(nonceSize) { | ||
const result = []; | ||
const chars = this.nonceChars; | ||
let charPos; | ||
const nonceCharsLength = chars.length; | ||
for (let i = 0; i < nonceSize; i++) { | ||
charPos = Math.floor(Math.random() * nonceCharsLength); | ||
result[i] = chars[charPos]; | ||
} | ||
return result.join(''); | ||
} | ||
_prepareParameters(token, tokenSecret, method, url, extraParams, body) { | ||
const 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; | ||
} | ||
let sig; | ||
if (this._isEcho) { | ||
sig = this.getSignature(this.signatureMethod, 'GET', this._verifyCredentials, oauthParameters, tokenSecret, body); | ||
} else { | ||
if (extraParams) { | ||
Object.keys(extraParams).forEach((key) => { | ||
oauthParameters[key] = extraParams[key]; | ||
}); | ||
} | ||
sig = this.getSignature(this.signatureMethod, method, url, oauthParameters, tokenSecret, body); | ||
} | ||
const orderedParameters = this._sortRequestParams(this._makeArrayOfArgumentsHash(oauthParameters)); | ||
orderedParameters[orderedParameters.length] = ['oauth_signature', sig]; | ||
return orderedParameters; | ||
} | ||
// Encodes parameters in the map. | ||
encodeUriParams(params) { | ||
const result = Object.keys(params).map((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(request, token, tokenSecret) { | ||
if (!request || !request.method || !request.url) { | ||
return request; | ||
} | ||
token = token || this.lastIssuedToken.oauth_token; | ||
tokenSecret = tokenSecret || this.lastIssuedToken.oauth_token_secret; | ||
let method = request.method || 'GET'; | ||
method = method.toUpperCase(); | ||
const withPayload = ['GET', 'HEAD'].indexOf(request.method) === -1; | ||
let body; | ||
if (withPayload && request.headers && request.body) { | ||
let contentType; | ||
try { | ||
contentType = this.getContentType(request.headers); | ||
} catch (_) { | ||
noop(); | ||
} | ||
if (contentType && contentType.indexOf(this.urlEncodedType) === 0) { | ||
body = request.body; | ||
} | ||
} | ||
const orderedParameters = this._prepareParameters(token, tokenSecret, method, request.url, {}, body); | ||
if (this.authParamsLocation === 'authorization') { | ||
const authorization = this._buildAuthorizationHeaders(orderedParameters); | ||
try { | ||
request.headers = this.replaceHeaderValue(request.headers, 'authorization', authorization); | ||
} catch (_) { | ||
noop(); | ||
} | ||
} else { | ||
request.url = this._buildAuthorizationQueryStirng(request.url, orderedParameters); | ||
} | ||
this.clearRequestVariables(); | ||
return request; | ||
} | ||
_performRequest(token, tokenSecret, method, url, extraParams, body, contentType) { | ||
const withPayload = ['POST', 'PUT'].indexOf(method) !== -1; | ||
const orderedParameters = this._prepareParameters(token, tokenSecret, method, url, extraParams); | ||
if (withPayload && !contentType) { | ||
contentType = this.urlEncodedType; | ||
} | ||
const headers = {}; | ||
if (this.authParamsLocation === 'authorization') { | ||
const 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((key) => { | ||
headers[key] = this._headers[key]; | ||
}); | ||
} | ||
if (extraParams) { | ||
Object.keys(extraParams).forEach((key) => { | ||
if (this._isParameterNameAnOAuthParameter(key)) { | ||
delete extraParams[key]; | ||
} | ||
}); | ||
} | ||
if (withPayload && extraParams && !body && ['POST', 'PUT'].indexOf(method) !== -1) { | ||
body = this.encodeUriParams(extraParams); | ||
body = this._finishEncodeParams(body); | ||
} | ||
if (withPayload && !body) { | ||
headers['Content-length'] = '0'; | ||
} | ||
const init = { | ||
method: method, | ||
headers: headers | ||
}; | ||
if (withPayload && body) { | ||
init.body = body; | ||
} | ||
let responseHeaders; | ||
return this.request(url, init) | ||
.then((response) => { | ||
if (!response.ok) { | ||
throw new Error('Token request error ended with status ' + response.status); | ||
} | ||
responseHeaders = response.headers; | ||
return response.text(); | ||
}) | ||
.then((text) => { | ||
return { | ||
response: text, | ||
headers: responseHeaders | ||
}; | ||
}); | ||
} | ||
/** | ||
* Exchanges temporary authorization token for authorized token. | ||
* When ready this function fires `oauth1-token-response` | ||
* | ||
* @param {String} token | ||
* @param {String} secret | ||
* @param {String} verifier | ||
* @return {Promise} | ||
*/ | ||
getOAuthAccessToken(token, secret, verifier) { | ||
const extraParams = {}; | ||
if (verifier) { | ||
extraParams.oauth_verifier = verifier; | ||
} | ||
const method = this.authTokenMethod; | ||
return this._performRequest(token, secret, method, this.accessTokenUri, extraParams) | ||
.then((response) => { | ||
if (!response.response) { | ||
let message = "Couldn't exchange token. "; | ||
message += 'Authorization server may be down or CORS is disabled.'; | ||
throw new Error(message); | ||
} | ||
const params = {}; | ||
this._formUrlEncodedToParams(response.response).forEach((pair) => { | ||
params[pair[0]] = pair[1]; | ||
}); | ||
return params; | ||
}) | ||
.then((tokenInfo) => { | ||
this.clearRequestVariables(); | ||
this.lastIssuedToken = tokenInfo; | ||
const e = new CustomEvent('oauth1-token-response', { | ||
bubbles: true, | ||
composed: true, | ||
cancelable: false, | ||
detail: tokenInfo | ||
}); | ||
this.dispatchEvent(e); | ||
}); | ||
} | ||
/** | ||
* Clears variables set for current request after signature has been | ||
* generated and token obtained. | ||
*/ | ||
clearRequestVariables() { | ||
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(extraParams) { | ||
extraParams = extraParams || {}; | ||
if (this._authorizeCallback) { | ||
extraParams.oauth_callback = this._authorizeCallback; | ||
} | ||
const method = this.authTokenMethod; | ||
return this._performRequest(null, null, method, this.requestTokenUri, extraParams).then((response) => { | ||
if (!response.response) { | ||
let message = "Couldn't request for authorization token. "; | ||
message += 'Authorization server may be down or CORS is disabled.'; | ||
throw new Error(message); | ||
} | ||
const params = {}; | ||
this._formUrlEncodedToParams(response.response).forEach((pair) => { | ||
params[pair[0]] = pair[1]; | ||
}); | ||
return params; | ||
}); | ||
} | ||
/** | ||
* 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(url, init) { | ||
const e = new CustomEvent('auth-request-proxy', { | ||
bubbles: true, | ||
composed: true, | ||
cancelable: true, | ||
detail: { | ||
url: url, | ||
init: init | ||
} | ||
}); | ||
this.dispatchEvent(e); | ||
return e.defaultPrevented ? e.detail.result : this._fetch(url, init); | ||
} | ||
/** | ||
* 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(url, init) { | ||
let 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(e) { | ||
if ( | ||
!location || | ||
!e.source || | ||
!this._popup || | ||
e.origin !== location.origin || | ||
e.source.location.href !== this._popup.location.href | ||
) { | ||
return; | ||
} | ||
const 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() { | ||
const popupCheckInterval = setInterval(() => { | ||
if (!this._popup || this._popup.closed) { | ||
clearInterval(popupCheckInterval); | ||
this._beforePopupUnloadHandler(); | ||
} | ||
}, 500); | ||
} | ||
_beforePopupUnloadHandler() { | ||
if (this.popupClosedProperly) { | ||
return; | ||
} | ||
this._popup = undefined; | ||
this._dispatchError('No response has been recorded.', 'no-response'); | ||
} | ||
/** | ||
* Dispatches an error event that propagates through the DOM. | ||
* | ||
* @param {String} message | ||
* @param {String} code | ||
*/ | ||
_dispatchError(message, code) { | ||
const e = new CustomEvent('oauth1-error', { | ||
bubbles: true, | ||
composed: true, | ||
detail: { | ||
message: message, | ||
code: code | ||
} | ||
}); | ||
this.dispatchEvent(e); | ||
} | ||
/** | ||
* Adds camel case keys to a map of parameters. | ||
* It adds new keys to the object tranformed from `oauth_token` | ||
* to `oauthToken` | ||
* | ||
* @param {Object} obj | ||
* @return {Object} | ||
*/ | ||
parseMapKeys(obj) { | ||
Object.keys(obj).forEach((key) => this._parseParameter(key, obj)); | ||
return obj; | ||
} | ||
/** | ||
* Parses a query parameter object to produce camel case map of parameters. | ||
* This sets values to the `settings` object which is passed by reference. | ||
* No need to return value. | ||
* | ||
* @param {String} param Key in the `settings` object. | ||
* @param {Object} settings Parameters. | ||
* @return {Object} | ||
*/ | ||
_parseParameter(param, settings) { | ||
if (!(param in settings)) { | ||
return settings; | ||
} | ||
const value = settings[param]; | ||
let oauthParam; | ||
if (this._caseMap[param]) { | ||
oauthParam = this._caseMap[param]; | ||
} else { | ||
oauthParam = this._getCaseParam(param); | ||
} | ||
settings[oauthParam] = value; | ||
} | ||
_getCaseParam(param) { | ||
return 'oauth_' + param.replace(this._camelRegex, '_$1').toLowerCase(); | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
/** | ||
* Dispatched when the component requests to proxy authorization request | ||
* through proxy. If the application decide to proxy the request it must | ||
* cancel the events. | ||
* | ||
* The handler must set `event.detail.result` property to be a `Promise` | ||
* with call result that will be reported to the application. | ||
* | ||
* It can be used to proxy CORS requests if the application can support this | ||
* case. | ||
* | ||
* @event auth-request-proxy | ||
* @param {String} url The request URL | ||
* @param {Object} init The same `init` object as the one used to initialize | ||
* `Request` object for fetch API. | ||
*/ | ||
} | ||
import { OAuth1Authorization } from './src/OAuth1Authorization.js'; | ||
export { OAuth1Authorization }; | ||
window.customElements.define('oauth1-authorization', OAuth1Authorization); |
@@ -13,348 +13,5 @@ /** | ||
// tslint:disable:variable-name Describing an API that's defined elsewhere. | ||
// tslint:disable:no-any describes the API as best we are able today | ||
import {OAuth2Authorization} from './src/OAuth2Authorization.js'; | ||
export {OAuth2Authorization}; | ||
declare namespace LogicElements { | ||
/** | ||
* The `<outh2-authorization>` performs an OAuth2 requests to get a token for given settings. | ||
*/ | ||
class OAuth2Authorization extends HTMLElement { | ||
readonly tokenInfo: object|null; | ||
ontokenerror: Function|null; | ||
ontokenresponse: Function|null; | ||
connectedCallback(): void; | ||
disconnectedCallback(): void; | ||
/** | ||
* Clears the state of the element. | ||
*/ | ||
clear(): void; | ||
/** | ||
* Clean up popup reference and closes the window if not yet closed. | ||
*/ | ||
_cleanupPopup(): void; | ||
/** | ||
* Handler for the `oauth2-token-requested` custom event. | ||
*/ | ||
_tokenRequestedHandler(e: CustomEvent|null): void; | ||
/** | ||
* Authorize the user using provided settings. | ||
* | ||
* @param settings Map of authorization settings. | ||
* - type {String} Authorization grant type. Can be `implicit`, | ||
* `authorization_code`, `client_credentials`, `password` or custom value | ||
* as OAuth 2.0 allows extensions to grant type. | ||
*/ | ||
authorize(settings: {[key: String|null]: String|null}): void; | ||
/** | ||
* Checks if basic configuration of the OAuth 2 request is valid an can proceed | ||
* with authentication. | ||
* | ||
* @param settings authorization settings | ||
*/ | ||
_sanityCheck(settings: object|null): void; | ||
/** | ||
* Checks if the URL has valid scheme for OAuth flow. | ||
* | ||
* @param url The url value to test | ||
*/ | ||
_checkUrl(url: String|null): void; | ||
/** | ||
* Authorizes the user in the OAuth authorization endpoint. | ||
* By default it authorizes the user using a popup that displays | ||
* authorization screen. When `interactive` property is set to `false` | ||
* on the `settings` object then it will quietly create an iframe | ||
* and try to receive the token. | ||
* | ||
* @param authUrl Complete authorization url | ||
* @param settings Passed user settings | ||
*/ | ||
_authorize(authUrl: String|null, settings: object|null): void; | ||
/** | ||
* Creates and opens auth popup. | ||
* | ||
* @param url Complete authorization url | ||
*/ | ||
_authorizePopup(url: String|null): void; | ||
/** | ||
* Tries to Authorize the user in a non interactive way. | ||
* This method always result in a success response. When there's an error or | ||
* user is not logged in then the response won't contain auth token info. | ||
* | ||
* @param url Complete authorization url | ||
*/ | ||
_authorizeTokenNonInteractive(url: String|null): void; | ||
/** | ||
* Removes the frame and any event listeners attached to it. | ||
*/ | ||
_cleanupFrame(): void; | ||
/** | ||
* Handler for `error` event dispatched by oauth iframe. | ||
*/ | ||
_frameLoadErrorHandler(): void; | ||
/** | ||
* Handler for iframe `load` event. | ||
*/ | ||
_frameLoadHandler(): void; | ||
/** | ||
* Observer if the popup has been closed befor the data has been received. | ||
*/ | ||
_observePopupState(): void; | ||
/** | ||
* Function called in the interval. | ||
* Observer popup state and calls `_beforePopupUnloadHandler()` | ||
* when popup is no longer opened. | ||
*/ | ||
_popupObserver(): void; | ||
/** | ||
* Browser or server flow: open the initial popup. | ||
* | ||
* @param settings Settings passed to the authorize function. | ||
* @param type `token` or `code` | ||
* @returns Full URL for the endpoint. | ||
*/ | ||
_constructPopupUrl(settings: object|null, type: String|null): String|null; | ||
/** | ||
* Computes `scope` URL parameter from scopes array. | ||
* | ||
* @param scopes List of scopes to use with the request. | ||
* @returns Computed scope value. | ||
*/ | ||
_computeScope(scopes: Array<String|null>|null): String|null; | ||
/** | ||
* Listens for a message from the popup. | ||
*/ | ||
_popupMessageHandler(e: Event|null): void; | ||
_processPopupData(e: any): void; | ||
_clearIframeTimeout(): void; | ||
/** | ||
* http://stackoverflow.com/a/10727155/1127848 | ||
*/ | ||
randomString(len: any): any; | ||
/** | ||
* Popup is closed by this element so if data is not yet set it means that the | ||
* user closed the window - probably some error. | ||
* The UI state is reset if needed. | ||
*/ | ||
_beforePopupUnloadHandler(): void; | ||
/** | ||
* Exchange code for token. | ||
* One note here. This element is intened to use with applications that test endpoints. | ||
* It asks user to provide `client_secret` parameter and it is not a security concern to him. | ||
* However, this method **can't be used in regular web applications** because it is a | ||
* security risk and whole OAuth token exchange can be compromised. Secrets should never be | ||
* present on client side. | ||
* | ||
* @param code Returned code from the authorization endpoint. | ||
* @returns Promise with token information. | ||
*/ | ||
_exchangeCode(code: String|null): Promise<any>|null; | ||
/** | ||
* Returns a body value for the code exchange request. | ||
* | ||
* @param settings Initial settings object. | ||
* @param code Authorization code value returned by the authorization | ||
* server. | ||
* @returns Request body. | ||
*/ | ||
_getCodeEchangeBody(settings: object|null, code: String|null): String|null; | ||
/** | ||
* Requests for token from the authorization server for `code`, `password`, | ||
* `client_credentials` and custom grant types. | ||
* | ||
* @param url Base URI of the endpoint. Custom properties will be | ||
* applied to the final URL. | ||
* @param body Generated body for given type. Custom properties will | ||
* be applied to the final body. | ||
* @param settings Settings object passed to the `authorize()` function | ||
* @returns Promise resolved to the response string. | ||
*/ | ||
_requestToken(url: String|null, body: String|null, settings: object|null): Promise<any>|null; | ||
/** | ||
* Handler for the code request load event. | ||
* Processes the response and either rejects the promise with an error | ||
* or resolves it to token info object. | ||
* | ||
* @param e XHR load event. | ||
* @param resolve Resolve function | ||
* @param reject Reject function | ||
*/ | ||
_processTokenResponseHandler(e: Event|null, resolve: Function|null, reject: Function|null): void; | ||
/** | ||
* Handler for the code request error event. | ||
* Rejects the promise with error description. | ||
* | ||
* @param e XHR error event | ||
* @param reject Promise's reject function. | ||
*/ | ||
_processTokenResponseErrorHandler(e: Event|null, reject: Function|null): void; | ||
/** | ||
* Processes token request body and produces map of values. | ||
* | ||
* @param body Body received in the response. | ||
* @param contentType Response content type. | ||
* @returns Response as an object. | ||
*/ | ||
_processCodeResponse(body: String|null, contentType: String|null): object|null; | ||
/** | ||
* Processes token info object when it's ready. | ||
* Sets `tokenInfo` property, notifies listeners about the response | ||
* and cleans up. | ||
* | ||
* @param tokenInfo Token info returned from the server. | ||
* @returns The same tokenInfo, used for Promise return value. | ||
*/ | ||
_handleTokenInfo(tokenInfo: object|null): object|null; | ||
/** | ||
* Handler fore an error that happened during code exchange. | ||
*/ | ||
_handleTokenCodeError(e: Error|null): void; | ||
/** | ||
* Replaces `-` or `_` with camel case. | ||
* | ||
* @param name The string to process | ||
* @returns Camel cased string or `undefined` if not | ||
* transformed. | ||
*/ | ||
_camel(name: String|null): String|null|undefined; | ||
/** | ||
* Requests a token for `password` request type. | ||
* | ||
* @param settings The same settings as passed to `authorize()` | ||
* function. | ||
* @returns Promise resolved to token info. | ||
*/ | ||
authorizePassword(settings: object|null): Promise<any>|null; | ||
/** | ||
* Generates a payload message for password authorization. | ||
* | ||
* @param settings Settings object passed to the `authorize()` | ||
* function | ||
* @returns Message body as defined in OAuth2 spec. | ||
*/ | ||
_getPasswordBody(settings: object|null): String|null; | ||
/** | ||
* Requests a token for `client_credentials` request type. | ||
* | ||
* @param settings The same settings as passed to `authorize()` | ||
* function. | ||
* @returns Promise resolved to a token info object. | ||
*/ | ||
authorizeClientCredentials(settings: object|null): Promise<any>|null; | ||
/** | ||
* Generates a payload message for client credentials. | ||
* | ||
* @param settings Settings object passed to the `authorize()` | ||
* function | ||
* @returns Message body as defined in OAuth2 spec. | ||
*/ | ||
_getClientCredentialsBody(settings: object|null): String|null; | ||
/** | ||
* Performs authorization on custom grant type. | ||
* This extension is described in OAuth 2.0 spec. | ||
* | ||
* @param settings Settings object as for `authorize()` function. | ||
* @returns Promise resolved to a token info object. | ||
*/ | ||
authorizeCustomGrant(settings: object|null): Promise<any>|null; | ||
/** | ||
* Creates a body for custom gran type. | ||
* It does not assume any parameter to be required. | ||
* It applies all known OAuth 2.0 parameters and then custom parameters | ||
* | ||
* @returns Request body. | ||
*/ | ||
_getCustomGrantBody(settings: object|null): String|null; | ||
/** | ||
* Applies custom properties defined in the OAuth settings object to the URL. | ||
* | ||
* @param url Generated URL for an endpoint. | ||
* @param data `customData.[type]` property from the settings object. | ||
* The type is either `auth` or `token`. | ||
*/ | ||
_applyCustomSettingsQuery(url: String|null, data: object|null): String|null; | ||
/** | ||
* Applies custom headers from the settings object | ||
* | ||
* @param xhr Instance of the request object. | ||
* @param data Value of settings' `customData` property | ||
*/ | ||
_applyCustomSettingsHeaders(xhr: XMLHttpRequest|null, data: object|null): void; | ||
/** | ||
* Applies custom body properties from the settings to the body value. | ||
* | ||
* @param body Already computed body for OAuth request. Custom | ||
* properties are appended at the end of OAuth string. | ||
* @param data Value of settings' `customData` property | ||
* @returns Request body | ||
*/ | ||
_applyCustomSettingsBody(body: String|null, data: object|null): String|null; | ||
/** | ||
* Dispatches an error event that propagates through the DOM. | ||
* | ||
* @param detail The detail object. | ||
*/ | ||
_dispatchError(detail: object|null): void; | ||
/** | ||
* Dispatches an error event that propagates through the DOM. | ||
* | ||
* @param detail The detail object. | ||
*/ | ||
_dispatchResponse(detail: object|null): void; | ||
/** | ||
* Registers an event handler for given type | ||
* | ||
* @param eventType Event type (name) | ||
* @param value The handler to register | ||
*/ | ||
_registerCallback(eventType: String|null, value: Function|null): void; | ||
} | ||
} | ||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"oauth2-authorization": LogicElements.OAuth2Authorization; | ||
} | ||
} |
@@ -14,986 +14,4 @@ /** | ||
*/ | ||
function noop() {} | ||
/** | ||
The `<outh2-authorization>` performs an OAuth2 requests to get a token for given settings. | ||
@customElement | ||
@memberof LogicElements | ||
*/ | ||
export class OAuth2Authorization extends HTMLElement { | ||
/** | ||
* @return {Object} A full data returned by the authorization endpoint. | ||
*/ | ||
get tokenInfo() { | ||
return this._tokenInfo; | ||
} | ||
constructor() { | ||
super(); | ||
this._frameLoadErrorHandler = this._frameLoadErrorHandler.bind(this); | ||
this._frameLoadHandler = this._frameLoadHandler.bind(this); | ||
this._tokenRequestedHandler = this._tokenRequestedHandler.bind(this); | ||
this._popupMessageHandler = this._popupMessageHandler.bind(this); | ||
this._popupObserver = this._popupObserver.bind(this); | ||
} | ||
connectedCallback() { | ||
window.addEventListener('oauth2-token-requested', this._tokenRequestedHandler); | ||
window.addEventListener('message', this._popupMessageHandler); | ||
this.setAttribute('aria-hidden', 'true'); | ||
} | ||
disconnectedCallback() { | ||
window.removeEventListener('oauth2-token-requested', this._tokenRequestedHandler); | ||
window.removeEventListener('message', this._popupMessageHandler); | ||
} | ||
/** | ||
* Clears the state of the element. | ||
*/ | ||
clear() { | ||
this._state = undefined; | ||
this._settings = undefined; | ||
this._cleanupFrame(); | ||
this._cleanupPopup(); | ||
} | ||
/** | ||
* Clean up popup reference and closes the window if not yet closed. | ||
*/ | ||
_cleanupPopup() { | ||
if (this._popup) { | ||
if (!this._popup.closed) { | ||
this._popup.close(); | ||
} | ||
this._popup = undefined; | ||
} | ||
} | ||
/** | ||
* Handler for the `oauth2-token-requested` custom event. | ||
* | ||
* @param {CustomEvent} e | ||
*/ | ||
_tokenRequestedHandler(e) { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
e.stopImmediatePropagation(); | ||
this.authorize(e.detail); | ||
} | ||
/** | ||
* Authorize the user using provided settings. | ||
* | ||
* @param {Object<String, String>} settings Map of authorization settings. | ||
* - type {String} Authorization grant type. Can be `implicit`, | ||
* `authorization_code`, `client_credentials`, `password` or custom value | ||
* as OAuth 2.0 allows extensions to grant type. | ||
* | ||
* NOTE: | ||
* For authorization_code and any other grant type that may receive a code | ||
* and exchange it for an access token, the settings object may have a property | ||
* "overrideExchangeCodeFlow" with a boolean value (true/false). | ||
* | ||
* The "overrideExchangeCodeFlow" property is a flag indicating that the developer wants to handle | ||
* exchanging the code for the token instead of having the module do it. | ||
* | ||
* If "overrideExchangeCodeFlow" is set to true for the authorization_code grant type, | ||
* we dispatch an "oauth2-code-response" event with the auth code. | ||
* | ||
* The user of this module should listen for this event and exchange the token for an access token on their end. | ||
* | ||
* This allows client-side apps to exchange the auth code with their backend/server for an access token | ||
* since CORS isn't enabled for the /token endpoint. | ||
*/ | ||
authorize(settings) { | ||
this._tokenInfo = undefined; | ||
this._type = settings.type; | ||
this._state = settings.state || this.randomString(6); | ||
this._settings = settings; | ||
this._errored = false; | ||
this._overrideExchangeCodeFlow = settings.overrideExchangeCodeFlow; | ||
try { | ||
this._sanityCheck(settings); | ||
} catch (e) { | ||
this._dispatchError({ | ||
message: e.message, | ||
code: 'oauth_error', | ||
state: this._state, | ||
interactive: settings.interactive | ||
}); | ||
throw e; | ||
} | ||
switch (settings.type) { | ||
case 'implicit': | ||
this._authorize(this._constructPopupUrl(settings, 'token'), settings); | ||
break; | ||
case 'authorization_code': | ||
this._authorize(this._constructPopupUrl(settings, 'code'), settings); | ||
break; | ||
case 'client_credentials': | ||
this.authorizeClientCredentials(settings).catch(() => {}); | ||
break; | ||
case 'password': | ||
this.authorizePassword(settings).catch(() => {}); | ||
break; | ||
default: | ||
this.authorizeCustomGrant(settings).catch(() => {}); | ||
} | ||
} | ||
/** | ||
* Checks if basic configuration of the OAuth 2 request is valid an can proceed | ||
* with authentication. | ||
* @param {Object} settings authorization settings | ||
* @throws {Error} When setttings are not valid | ||
*/ | ||
_sanityCheck(settings) { | ||
if (settings.type === 'implicit' || settings.type === 'authorization_code') { | ||
try { | ||
this._checkUrl(settings.authorizationUri); | ||
} catch (e) { | ||
throw new Error(`authorizationUri: ${e.message}`); | ||
} | ||
if (settings.accessTokenUri) { | ||
try { | ||
this._checkUrl(settings.accessTokenUri); | ||
} catch (e) { | ||
throw new Error(`accessTokenUri: ${e.message}`); | ||
} | ||
} | ||
} else if (settings.accessTokenUri) { | ||
if (settings.accessTokenUri) { | ||
try { | ||
this._checkUrl(settings.accessTokenUri); | ||
} catch (e) { | ||
throw new Error(`accessTokenUri: ${e.message}`); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Checks if the URL has valid scheme for OAuth flow. | ||
* @param {String} url The url value to test | ||
* @throws {TypeError} When passed value is not set, empty, or not a string | ||
* @throws {Error} When passed value is not a valid URL for OAuth 2 flow | ||
*/ | ||
_checkUrl(url) { | ||
if (!url) { | ||
throw new TypeError('the value is missing'); | ||
} | ||
if (typeof url !== 'string') { | ||
throw new TypeError('the value is not a string'); | ||
} | ||
if (url.indexOf('http://') === -1 && url.indexOf('https://') === -1) { | ||
throw new Error('the value has invalid scheme'); | ||
} | ||
} | ||
/** | ||
* Authorizes the user in the OAuth authorization endpoint. | ||
* By default it authorizes the user using a popup that displays | ||
* authorization screen. When `interactive` property is set to `false` | ||
* on the `settings` object then it will quietly create an iframe | ||
* and try to receive the token. | ||
* | ||
* @param {String} authUrl Complete authorization url | ||
* @param {Object} settings Passed user settings | ||
*/ | ||
_authorize(authUrl, settings) { | ||
this._settings = settings; | ||
this._errored = false; | ||
if (settings.interactive === false) { | ||
this._authorizeTokenNonInteractive(authUrl); | ||
} else { | ||
this._authorizePopup(authUrl); | ||
} | ||
} | ||
/** | ||
* Creates and opens auth popup. | ||
* | ||
* @param {String} url Complete authorization url | ||
*/ | ||
_authorizePopup(url) { | ||
const op = 'menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,width=800,height=600'; | ||
this._popup = window.open(url, 'oauth-window', op); | ||
if (!this._popup) { | ||
// popup blocked. | ||
this._dispatchError({ | ||
message: 'Authorization popup is being blocked.', | ||
code: 'popup_blocked', | ||
state: this._state, | ||
interactive: this._settings.interactive | ||
}); | ||
return; | ||
} | ||
this._popup.window.focus(); | ||
this._observePopupState(); | ||
} | ||
/** | ||
* Tries to Authorize the user in a non interactive way. | ||
* This method always result in a success response. When there's an error or | ||
* user is not logged in then the response won't contain auth token info. | ||
* | ||
* @param {String} url Complete authorization url | ||
*/ | ||
_authorizeTokenNonInteractive(url) { | ||
const iframe = document.createElement('iframe'); | ||
iframe.style.border = '0'; | ||
iframe.style.width = '0'; | ||
iframe.style.height = '0'; | ||
iframe.style.overflow = 'hidden'; | ||
iframe.addEventListener('error', this._frameLoadErrorHandler); | ||
iframe.addEventListener('load', this._frameLoadHandler); | ||
iframe.id = 'oauth2-authorization-frame'; | ||
iframe.setAttribute('data-owner', 'arc-oauth-authorization'); | ||
document.body.appendChild(iframe); | ||
iframe.src = url; | ||
this._iframe = iframe; | ||
} | ||
/** | ||
* Removes the frame and any event listeners attached to it. | ||
*/ | ||
_cleanupFrame() { | ||
if (!this._iframe) { | ||
return; | ||
} | ||
this._iframe.removeEventListener('error', this._frameLoadErrorHandler); | ||
this._iframe.removeEventListener('load', this._frameLoadHandler); | ||
try { | ||
document.body.removeChild(this._iframe); | ||
} catch (_) { | ||
noop(); | ||
} | ||
this._iframe = undefined; | ||
} | ||
/** | ||
* Handler for `error` event dispatched by oauth iframe. | ||
*/ | ||
_frameLoadErrorHandler() { | ||
if (this._errored) { | ||
return; | ||
} | ||
this._dispatchResponse({ | ||
interactive: false, | ||
code: 'iframe_load_error', | ||
state: this._state | ||
}); | ||
this.clear(); | ||
} | ||
/** | ||
* Handler for iframe `load` event. | ||
*/ | ||
_frameLoadHandler() { | ||
if (this.__frameLoadInfo) { | ||
return; | ||
} | ||
this.__frameLoadInfo = true; | ||
this.__frameLoadTimeout = setTimeout(() => { | ||
if (!this.tokenInfo && !this._errored) { | ||
this._dispatchResponse({ | ||
interactive: false, | ||
code: 'not_authorized', | ||
state: this._state | ||
}); | ||
} | ||
this.clear(); | ||
this.__frameLoadInfo = false; | ||
}, 700); | ||
} | ||
// Observer if the popup has been closed befor the data has been received. | ||
_observePopupState() { | ||
this.__popupCheckInterval = setInterval(this._popupObserver, 250); | ||
} | ||
/** | ||
* Function called in the interval. | ||
* Observer popup state and calls `_beforePopupUnloadHandler()` | ||
* when popup is no longer opened. | ||
*/ | ||
_popupObserver() { | ||
if (!this._popup || this._popup.closed) { | ||
clearInterval(this.__popupCheckInterval); | ||
this.__popupCheckInterval = undefined; | ||
this._beforePopupUnloadHandler(); | ||
} | ||
} | ||
/** | ||
* Browser or server flow: open the initial popup. | ||
* @param {Object} settings Settings passed to the authorize function. | ||
* @param {String} type `token` or `code` | ||
* @return {String} Full URL for the endpoint. | ||
*/ | ||
_constructPopupUrl(settings, type) { | ||
let url = settings.authorizationUri; | ||
if (url.indexOf('?') === -1) { | ||
url += '?'; | ||
} else { | ||
url += '&'; | ||
} | ||
url += 'response_type=' + type; | ||
url += '&client_id=' + encodeURIComponent(settings.clientId || ''); | ||
if (settings.redirectUri) { | ||
url += '&redirect_uri=' + encodeURIComponent(settings.redirectUri); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
url += '&scope=' + this._computeScope(settings.scopes); | ||
} | ||
url += '&state=' + encodeURIComponent(this._state); | ||
if (settings.includeGrantedScopes) { | ||
url += '&include_granted_scopes=true'; | ||
} | ||
if (settings.loginHint) { | ||
url += '&login_hint=' + encodeURIComponent(settings.loginHint); | ||
} | ||
if (settings.interactive === false) { | ||
url += '&prompt=none'; | ||
} | ||
// custom query parameters | ||
if (settings.customData) { | ||
const key = type === 'token' ? 'auth' : 'token'; | ||
const cs = settings.customData[key]; | ||
if (cs) { | ||
url = this._applyCustomSettingsQuery(url, cs); | ||
} | ||
} | ||
return url; | ||
} | ||
/** | ||
* Computes `scope` URL parameter from scopes array. | ||
* | ||
* @param {Array<String>} scopes List of scopes to use with the request. | ||
* @return {String} Computed scope value. | ||
*/ | ||
_computeScope(scopes) { | ||
if (!scopes) { | ||
return ''; | ||
} | ||
const scope = scopes.join(' '); | ||
return encodeURIComponent(scope); | ||
} | ||
/** | ||
* Listens for a message from the popup. | ||
* @param {Event} e | ||
*/ | ||
_popupMessageHandler(e) { | ||
if (!this._popup && !this._iframe) { | ||
return; | ||
} | ||
this._processPopupData(e); | ||
} | ||
_processPopupData(e) { | ||
const tokenInfo = e.data; | ||
const dontProcess = !this._overrideExchangeCodeFlow && (!tokenInfo || !tokenInfo.oauth2response); | ||
if (dontProcess) { | ||
// Possibly a message in the authorization info, not the popup. | ||
return; | ||
} | ||
if (!this._settings) { | ||
this._settings = {}; | ||
} | ||
if (tokenInfo.state !== this._state) { | ||
this._dispatchError({ | ||
message: 'Invalid state returned by the OAuth server.', | ||
code: 'invalid_state', | ||
state: this._state, | ||
serverState: tokenInfo.state, | ||
interactive: this._settings.interactive | ||
}); | ||
this._errored = true; | ||
this._clearIframeTimeout(); | ||
this.clear(); | ||
} else if ('error' in tokenInfo) { | ||
this._dispatchError({ | ||
message: tokenInfo.errorDescription || 'The request is invalid.', | ||
code: tokenInfo.error || 'oauth_error', | ||
state: this._state, | ||
interactive: this._settings.interactive | ||
}); | ||
this._errored = true; | ||
this._clearIframeTimeout(); | ||
this.clear(); | ||
} else if (this._type === 'implicit') { | ||
this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
} else if (this._type === 'authorization_code') { | ||
/** | ||
* For the authorization_code flow, the developer (user of the oauth2-authorization lib) | ||
* can pass a setting to override the code exchange flow. In this scenario, | ||
* we dispatch an event with the auth code instead of exchanging the code for an access token. | ||
* See {@link authorize()} comment for more details. | ||
*/ | ||
if (this._overrideExchangeCodeFlow) { | ||
this._dispatchCodeResponse(tokenInfo); | ||
} else { | ||
this._exchangeCodeValue = tokenInfo.code; | ||
this._exchangeCode(tokenInfo.code).catch(() => {}); | ||
this._clearIframeTimeout(); | ||
} | ||
} | ||
} | ||
_clearIframeTimeout() { | ||
if (this.__frameLoadTimeout) { | ||
clearTimeout(this.__frameLoadTimeout); | ||
this.__frameLoadTimeout = undefined; | ||
} | ||
} | ||
// http://stackoverflow.com/a/10727155/1127848 | ||
randomString(len) { | ||
return Math.round(Math.pow(36, len + 1) - Math.random() * Math.pow(36, len)) | ||
.toString(36) | ||
.slice(1); | ||
} | ||
/** | ||
* Popup is closed by this element so if data is not yet set it means that the | ||
* user closed the window - probably some error. | ||
* The UI state is reset if needed. | ||
*/ | ||
_beforePopupUnloadHandler() { | ||
if (this.tokenInfo || (this._type === 'authorization_code' && this._exchangeCodeValue)) { | ||
return; | ||
} | ||
const settings = this._settings || {}; | ||
this._dispatchError({ | ||
message: 'No response has been recorded.', | ||
code: 'no_response', | ||
state: this._state, | ||
interactive: settings.interactive | ||
}); | ||
this.clear(); | ||
} | ||
/** | ||
* Exchange code for token. | ||
* One note here. This element is intened to use with applications that test endpoints. | ||
* It asks user to provide `client_secret` parameter and it is not a security concern to him. | ||
* However, this method **can't be used in regular web applications** because it is a | ||
* security risk and whole OAuth token exchange can be compromised. Secrets should never be | ||
* present on client side. | ||
* | ||
* @param {String} code Returned code from the authorization endpoint. | ||
* @return {Promise} Promise with token information. | ||
*/ | ||
async _exchangeCode(code) { | ||
const url = this._settings.accessTokenUri; | ||
const body = this._getCodeEchangeBody(this._settings, code); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, this._settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
} catch (cause) { | ||
this._handleTokenCodeError(cause); | ||
} | ||
} | ||
/** | ||
* Returns a body value for the code exchange request. | ||
* @param {Object} settings Initial settings object. | ||
* @param {String} code Authorization code value returned by the authorization | ||
* server. | ||
* @return {String} Request body. | ||
*/ | ||
_getCodeEchangeBody(settings, code) { | ||
let url = 'grant_type=authorization_code'; | ||
url += '&client_id=' + encodeURIComponent(settings.clientId); | ||
if (settings.redirectUri) { | ||
url += '&redirect_uri=' + encodeURIComponent(settings.redirectUri); | ||
} | ||
url += '&code=' + encodeURIComponent(code); | ||
if (settings.clientSecret) { | ||
url += '&client_secret=' + encodeURIComponent(settings.clientSecret); | ||
} else { | ||
url += '&client_secret='; | ||
} | ||
return url; | ||
} | ||
/** | ||
* Requests for token from the authorization server for `code`, `password`, | ||
* `client_credentials` and custom grant types. | ||
* | ||
* @param {String} url Base URI of the endpoint. Custom properties will be | ||
* applied to the final URL. | ||
* @param {String} body Generated body for given type. Custom properties will | ||
* be applied to the final body. | ||
* @param {Object} settings Settings object passed to the `authorize()` function | ||
* @return {Promise} Promise resolved to the response string. | ||
*/ | ||
_requestToken(url, body, settings) { | ||
if (settings.customData) { | ||
const cs = settings.customData.token; | ||
if (cs) { | ||
url = this._applyCustomSettingsQuery(url, cs); | ||
} | ||
body = this._applyCustomSettingsBody(body, settings.customData); | ||
} | ||
/* global Promise */ | ||
return new Promise((resolve, reject) => { | ||
const xhr = new XMLHttpRequest(); | ||
xhr.addEventListener('load', (e) => this._processTokenResponseHandler(e, resolve, reject)); | ||
xhr.addEventListener('error', (e) => this._processTokenResponseErrorHandler(e, reject)); | ||
xhr.open('POST', url); | ||
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); | ||
if (settings.customData) { | ||
this._applyCustomSettingsHeaders(xhr, settings.customData); | ||
} | ||
try { | ||
xhr.send(body); | ||
} catch (e) { | ||
reject(new Error('Client request error: ' + e.message)); | ||
} | ||
}); | ||
} | ||
/** | ||
* Handler for the code request load event. | ||
* Processes the response and either rejects the promise with an error | ||
* or resolves it to token info object. | ||
* | ||
* @param {Event} e XHR load event. | ||
* @param {Function} resolve Resolve function | ||
* @param {Function} reject Reject function | ||
*/ | ||
_processTokenResponseHandler(e, resolve, reject) { | ||
const status = e.target.status; | ||
const srvResponse = e.target.response; | ||
if (status === 404) { | ||
const message = 'Authorization URI is invalid. Received status 404.'; | ||
reject(new Error(message)); | ||
return; | ||
} else if (status >= 400 && status < 500) { | ||
const message = 'Client error: ' + srvResponse; | ||
reject(new Error(message)); | ||
return; | ||
} else if (status >= 500) { | ||
const message = 'Authorization server error. Response code is ' + status; | ||
reject(new Error(message)); | ||
return; | ||
} | ||
let tokenInfo; | ||
try { | ||
tokenInfo = this._processCodeResponse(srvResponse, e.target.getResponseHeader('content-type')); | ||
} catch (e) { | ||
reject(new Error(e.message)); | ||
return; | ||
} | ||
resolve(tokenInfo); | ||
} | ||
/** | ||
* Handler for the code request error event. | ||
* Rejects the promise with error description. | ||
* | ||
* @param {Event} e XHR error event | ||
* @param {Function} reject Promise's reject function. | ||
*/ | ||
_processTokenResponseErrorHandler(e, reject) { | ||
const status = e.target.status; | ||
let message = 'The request to the authorization server failed.'; | ||
if (status) { | ||
message += ' Response code is: ' + status; | ||
} | ||
reject(new Error(message)); | ||
} | ||
/** | ||
* Processes token request body and produces map of values. | ||
* | ||
* @param {String} body Body received in the response. | ||
* @param {String} contentType Response content type. | ||
* @return {Object} Response as an object. | ||
* @throws {Error} Exception when body is invalid. | ||
*/ | ||
_processCodeResponse(body, contentType) { | ||
if (!body) { | ||
throw new Error('Code response body is empty.'); | ||
} | ||
let tokenInfo; | ||
if (contentType.indexOf('json') !== -1) { | ||
tokenInfo = JSON.parse(body); | ||
Object.keys(tokenInfo).forEach((name) => { | ||
const camelName = this._camel(name); | ||
if (camelName) { | ||
tokenInfo[camelName] = tokenInfo[name]; | ||
} | ||
}); | ||
} else { | ||
tokenInfo = {}; | ||
body.split('&').forEach((p) => { | ||
const item = p.split('='); | ||
const name = item[0]; | ||
const camelName = this._camel(name); | ||
const value = decodeURIComponent(item[1]); | ||
tokenInfo[name] = value; | ||
tokenInfo[camelName] = value; | ||
}); | ||
} | ||
return tokenInfo; | ||
} | ||
/** | ||
* Processes token info object when it's ready. | ||
* Sets `tokenInfo` property, notifies listeners about the response | ||
* and cleans up. | ||
* | ||
* @param {Object} tokenInfo Token info returned from the server. | ||
* @return {Object} The same tokenInfo, used for Promise return value. | ||
*/ | ||
_handleTokenInfo(tokenInfo) { | ||
this._tokenInfo = tokenInfo; | ||
tokenInfo.interactive = this._settings.interactive; | ||
if ('error' in tokenInfo) { | ||
this._dispatchError({ | ||
message: tokenInfo.errorDescription || 'The request is invalid.', | ||
code: tokenInfo.error, | ||
state: this._state, | ||
interactive: this._settings.interactive | ||
}); | ||
} else { | ||
this._dispatchResponse(tokenInfo); | ||
} | ||
if (this.__frameLoadTimeout) { | ||
clearTimeout(this.__frameLoadTimeout); | ||
this.__frameLoadTimeout = undefined; | ||
} | ||
this._settings = undefined; | ||
this._exchangeCodeValue = undefined; | ||
return tokenInfo; | ||
} | ||
/** | ||
* Handler fore an error that happened during code exchange. | ||
* @param {Error} e | ||
*/ | ||
_handleTokenCodeError(e) { | ||
this._dispatchError({ | ||
message: "Couldn't connect to the server. " + e.message, | ||
code: 'request_error', | ||
state: this._state, | ||
interactive: this._settings.interactive | ||
}); | ||
this.clear(); | ||
throw e; | ||
} | ||
/** | ||
* Replaces `-` or `_` with camel case. | ||
* @param {String} name The string to process | ||
* @return {String|undefined} Camel cased string or `undefined` if not | ||
* transformed. | ||
*/ | ||
_camel(name) { | ||
let i = 0; | ||
let l; | ||
let changed = false; | ||
while ((l = name[i])) { | ||
if ((l === '_' || l === '-') && i + 1 < name.length) { | ||
name = name.substr(0, i) + name[i + 1].toUpperCase() + name.substr(i + 2); | ||
changed = true; | ||
} | ||
i++; | ||
} | ||
return changed ? name : undefined; | ||
} | ||
/** | ||
* Requests a token for `password` request type. | ||
* | ||
* @param {Object} settings The same settings as passed to `authorize()` | ||
* function. | ||
* @return {Promise} Promise resolved to token info. | ||
*/ | ||
async authorizePassword(settings) { | ||
this._settings = settings; | ||
const url = settings.accessTokenUri; | ||
const body = this._getPasswordBody(settings); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
} catch (cause) { | ||
this._handleTokenCodeError(cause); | ||
} | ||
} | ||
/** | ||
* Generates a payload message for password authorization. | ||
* | ||
* @param {Object} settings Settings object passed to the `authorize()` | ||
* function | ||
* @return {String} Message body as defined in OAuth2 spec. | ||
*/ | ||
_getPasswordBody(settings) { | ||
let url = 'grant_type=password'; | ||
url += '&username=' + encodeURIComponent(settings.username); | ||
url += '&password=' + encodeURIComponent(settings.password); | ||
if (settings.clientId) { | ||
url += '&client_id=' + encodeURIComponent(settings.clientId); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
url += '&scope=' + encodeURIComponent(settings.scopes.join(' ')); | ||
} | ||
return url; | ||
} | ||
/** | ||
* Requests a token for `client_credentials` request type. | ||
* | ||
* @param {Object} settings The same settings as passed to `authorize()` | ||
* function. | ||
* @return {Promise} Promise resolved to a token info object. | ||
*/ | ||
async authorizeClientCredentials(settings) { | ||
this._settings = settings; | ||
const url = settings.accessTokenUri; | ||
const body = this._getClientCredentialsBody(settings); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
} catch (cause) { | ||
this._handleTokenCodeError(cause); | ||
} | ||
} | ||
/** | ||
* Generates a payload message for client credentials. | ||
* | ||
* @param {Object} settings Settings object passed to the `authorize()` | ||
* function | ||
* @return {String} Message body as defined in OAuth2 spec. | ||
*/ | ||
_getClientCredentialsBody(settings) { | ||
let url = 'grant_type=client_credentials'; | ||
if (settings.clientId) { | ||
url += '&client_id=' + encodeURIComponent(settings.clientId); | ||
} | ||
if (settings.clientSecret) { | ||
url += '&client_secret=' + encodeURIComponent(settings.clientSecret); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
url += '&scope=' + this._computeScope(settings.scopes); | ||
} | ||
return url; | ||
} | ||
/** | ||
* Performs authorization on custom grant type. | ||
* This extension is described in OAuth 2.0 spec. | ||
* | ||
* @param {Object} settings Settings object as for `authorize()` function. | ||
* @return {Promise} Promise resolved to a token info object. | ||
*/ | ||
async authorizeCustomGrant(settings) { | ||
this._settings = settings; | ||
const url = settings.accessTokenUri; | ||
const body = this._getCustomGrantBody(settings); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
} catch (cause) { | ||
this._handleTokenCodeError(cause); | ||
} | ||
} | ||
/** | ||
* Creates a body for custom gran type. | ||
* It does not assume any parameter to be required. | ||
* It applies all known OAuth 2.0 parameters and then custom parameters | ||
* | ||
* @param {Object} settings | ||
* @return {String} Request body. | ||
*/ | ||
_getCustomGrantBody(settings) { | ||
const parts = ['grant_type=' + encodeURIComponent(settings.type)]; | ||
if (settings.clientId) { | ||
parts[parts.length] = 'client_id=' + encodeURIComponent(settings.clientId); | ||
} | ||
if (settings.clientSecret) { | ||
parts[parts.length] = 'client_secret=' + encodeURIComponent(settings.clientSecret); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
parts[parts.length] = 'scope=' + this._computeScope(settings.scopes); | ||
} | ||
if (settings.redirectUri) { | ||
parts[parts.length] = 'redirect_uri=' + encodeURIComponent(settings.redirectUri); | ||
} | ||
if (settings.username) { | ||
parts[parts.length] = 'username=' + encodeURIComponent(settings.username); | ||
} | ||
if (settings.password) { | ||
parts[parts.length] = 'password=' + encodeURIComponent(settings.password); | ||
} | ||
return parts.join('&'); | ||
} | ||
/** | ||
* Applies custom properties defined in the OAuth settings object to the URL. | ||
* | ||
* @param {String} url Generated URL for an endpoint. | ||
* @param {?Object} data `customData.[type]` property from the settings object. | ||
* The type is either `auth` or `token`. | ||
* @return {String} | ||
*/ | ||
_applyCustomSettingsQuery(url, data) { | ||
if (!data || !data.parameters) { | ||
return url; | ||
} | ||
url += url.indexOf('?') === -1 ? '?' : '&'; | ||
url += data.parameters | ||
.map((item) => { | ||
let value = item.value; | ||
if (value) { | ||
value = encodeURIComponent(value); | ||
} | ||
return encodeURIComponent(item.name) + '=' + value; | ||
}) | ||
.join('&'); | ||
return url; | ||
} | ||
/** | ||
* Applies custom headers from the settings object | ||
* | ||
* @param {XMLHttpRequest} xhr Instance of the request object. | ||
* @param {Object} data Value of settings' `customData` property | ||
*/ | ||
_applyCustomSettingsHeaders(xhr, data) { | ||
if (!data || !data.token || !data.token.headers) { | ||
return; | ||
} | ||
data.token.headers.forEach((item) => { | ||
try { | ||
xhr.setRequestHeader(item.name, item.value); | ||
} catch (e) { | ||
noop(); | ||
} | ||
}); | ||
} | ||
/** | ||
* Applies custom body properties from the settings to the body value. | ||
* | ||
* @param {String} body Already computed body for OAuth request. Custom | ||
* properties are appended at the end of OAuth string. | ||
* @param {Object} data Value of settings' `customData` property | ||
* @return {String} Request body | ||
*/ | ||
_applyCustomSettingsBody(body, data) { | ||
if (!data || !data.token || !data.token.body) { | ||
return body; | ||
} | ||
body += | ||
'&' + | ||
data.token.body | ||
.map(function(item) { | ||
let value = item.value; | ||
if (value) { | ||
value = encodeURIComponent(value); | ||
} | ||
return encodeURIComponent(item.name) + '=' + value; | ||
}) | ||
.join('&'); | ||
return body; | ||
} | ||
/** | ||
* Dispatches an error event that propagates through the DOM. | ||
* | ||
* @param {Object} detail The detail object. | ||
*/ | ||
_dispatchError(detail) { | ||
const e = new CustomEvent('oauth2-error', { | ||
bubbles: true, | ||
composed: true, | ||
detail | ||
}); | ||
this.dispatchEvent(e); | ||
} | ||
/** | ||
* Dispatches an event with the authorization code that propagates through the DOM. | ||
* Closes the popup once the authorization code has been dispatched. | ||
* | ||
* @param {Object} detail The detail object. | ||
*/ | ||
_dispatchCodeResponse(detail) { | ||
const e = new CustomEvent('oauth2-code-response', { | ||
bubbles: true, | ||
composed: true, | ||
detail | ||
}); | ||
this.dispatchEvent(e); | ||
this.clear(); | ||
} | ||
/** | ||
* Dispatches an event with the token (e.g. access token) that propagates through the DOM. | ||
* | ||
* @param {Object} detail The detail object. | ||
*/ | ||
_dispatchResponse(detail) { | ||
const e = new CustomEvent('oauth2-token-response', { | ||
bubbles: true, | ||
composed: true, | ||
detail | ||
}); | ||
this.dispatchEvent(e); | ||
} | ||
/** | ||
* @return {Function} Previously registered handler for `oauth2-error` event | ||
*/ | ||
get ontokenerror() { | ||
return this['_onoauth2-error']; | ||
} | ||
/** | ||
* Registers a callback function for `oauth2-error` event | ||
* @param {Function} value A callback to register. Pass `null` or `undefined` | ||
* to clear the listener. | ||
*/ | ||
set ontokenerror(value) { | ||
this._registerCallback('oauth2-error', value); | ||
} | ||
/** | ||
* @return {Function} Previously registered handler for `oauth2-token-response` event | ||
*/ | ||
get ontokenresponse() { | ||
return this['_onoauth2-token-response']; | ||
} | ||
/** | ||
* Registers a callback function for `oauth2-token-response` event | ||
* @param {Function} value A callback to register. Pass `null` or `undefined` | ||
* to clear the listener. | ||
*/ | ||
set ontokenresponse(value) { | ||
this._registerCallback('oauth2-token-response', value); | ||
} | ||
/** | ||
* Registers an event handler for given type | ||
* @param {String} eventType Event type (name) | ||
* @param {Function} value The handler to register | ||
*/ | ||
_registerCallback(eventType, value) { | ||
const key = `_on${eventType}`; | ||
if (this[key]) { | ||
this.removeEventListener(eventType, this[key]); | ||
} | ||
if (typeof value !== 'function') { | ||
this[key] = null; | ||
return; | ||
} | ||
this[key] = value; | ||
this.addEventListener(eventType, value); | ||
} | ||
/** | ||
* Fired when OAuth2 token has been received. | ||
* Properties of the `detail` object will contain the response from the authentication server. | ||
* It will contain the original parameteres but also camel case of the parameters. | ||
* | ||
* So for example 'implicit' will be in the response as well as `accessToken` with the same | ||
* value. The puropse of this is to support JS application that has strict formatting rules | ||
* and disallow using '_' in property names. Like ARC. | ||
* | ||
* @event oauth2-token-response | ||
*/ | ||
/** | ||
* Fired wne error occurred. | ||
* An error may occure when `state` parameter of the OAuth2 response is different from | ||
* the requested one. Another example is when the popup window has been closed before it passed | ||
* response token. It may happen when the OAuth request was invalid. | ||
* | ||
* @event oauth2-error | ||
* @param {String} message A message that can be displayed to the user. | ||
* @param {String} code A message code: `invalid_state` - when `state` parameter is different; | ||
* `no_response` when the popup was closed before sendin token data; `response_parse` - when | ||
* the response from the code exchange can't be parsed; `request_error` when the request | ||
* errored by the transport library. Other status codes are defined in | ||
* [rfc6749](https://tools.ietf.org/html/rfc6749). | ||
* @param {String} state The `state` parameter either generated by this element | ||
* when requesting the token or passed to the element from other element. | ||
*/ | ||
} | ||
import { OAuth2Authorization } from './src/OAuth2Authorization.js'; | ||
export { OAuth2Authorization }; | ||
window.customElements.define('oauth2-authorization', OAuth2Authorization); |
{ | ||
"name": "@advanced-rest-client/oauth-authorization", | ||
"description": "A set of elements that perform oauth authorization", | ||
"version": "3.0.2", | ||
"version": "4.0.0", | ||
"license": "Apache-2.0", | ||
@@ -30,2 +30,3 @@ "main": "outh-authorization.js", | ||
"dependencies": { | ||
"@advanced-rest-client/events-target-mixin": "^3.0.0", | ||
"@advanced-rest-client/headers-parser-mixin": "^3.0.0", | ||
@@ -73,7 +74,5 @@ "@polymer/iron-meta": "^3.0.0", | ||
"*.js": [ | ||
"eslint --fix", | ||
"prettier --write", | ||
"git add" | ||
"eslint --fix" | ||
] | ||
} | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
170149
18
3035
4
2
+ Added@advanced-rest-client/events-target-mixin@3.2.6(transitive)