@advanced-rest-client/oauth-authorization
Advanced tools
Comparing version 4.0.3 to 5.0.0
@@ -568,1 +568,39 @@ <a name="2.0.2"></a> | ||
<a name="5.0.0"></a> | ||
# [5.0.0](https://github.com/advanced-rest-client/oauth-authorization/compare/4.0.2...5.0.0) (2020-11-10) | ||
## Build | ||
* bumping version [7ee38eb](https://github.com/advanced-rest-client/oauth-authorization/commit/7ee38eb48d9e8bdec43d3cb01e43d8586100d984) by Pawel | ||
## Continuous integration | ||
* removing sudo from Travis config [d787ae2](https://github.com/advanced-rest-client/oauth-authorization/commit/d787ae2707d77e75cfd7f970713dd6a8e7b688e9) by Pawel | ||
## Update | ||
* removing previous ubuntu from tests [1ce9983](https://github.com/advanced-rest-client/oauth-authorization/commit/1ce99839fc18aa954e06959cdb3a2e57ade18252) by Pawel | ||
* playing with GH actions [fec47ca](https://github.com/advanced-rest-client/oauth-authorization/commit/fec47ca5ee22ef9cdea988735a05e292b74c819c) by Pawel | ||
* [ci skip] automated merge master->stage. syncing main branches [a5a3568](https://github.com/advanced-rest-client/oauth-authorization/commit/a5a356888117e3927d38510784e1cd84562e672e) by Ci agent | ||
* upgrading dependencies [73ff111](https://github.com/advanced-rest-client/oauth-authorization/commit/73ff111c5004c500ed37d05369fcb9d8de2ff79e) by Pawel | ||
* [ci skip] automated merge master->stage. syncing main branches [ee7e233](https://github.com/advanced-rest-client/oauth-authorization/commit/ee7e233b68286a053093bd2be1adc2068975163b) by Ci agent | ||
## Features | ||
* adding support for PKCE extension [f6f6585](https://github.com/advanced-rest-client/oauth-authorization/commit/f6f6585121d52b68f3171480c091dbe38550fc16) by Pawel | ||
## Bug Fixes | ||
* fixes #10 - secret with passwort grant [58bcb81](https://github.com/advanced-rest-client/oauth-authorization/commit/58bcb81dbdb2943bcd6e7763c2c69c34b68e173e) by Pawel | ||
* fixes APIC-398 - XSS with invalid auth URI [14f877b](https://github.com/advanced-rest-client/oauth-authorization/commit/14f877bee49ab420be56ea3db5a4a6a947de7ea2) by Pawel | ||
## Refactor | ||
* moving logic to a library [994c7d8](https://github.com/advanced-rest-client/oauth-authorization/commit/994c7d800906f3525490b2e5b03715d196c0e7c7) by Pawel | ||
@@ -1,16 +0,2 @@ | ||
/** | ||
* DO NOT EDIT | ||
* | ||
* This file was automatically generated by | ||
* https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations | ||
* | ||
* To modify these typings, edit the source file(s): | ||
* index.js | ||
*/ | ||
// tslint:disable:variable-name Describing an API that's defined elsewhere. | ||
export {OAuth2Authorization} from './src/OAuth2Authorization.js'; | ||
export {OAuth1Authorization} from './src/OAuth1Authorization.js'; | ||
export {OAuth2AuthorizationElement} from './src/OAuth2AuthorizationElement.js'; | ||
export {OAuth2Authorization} from './src/OAuth2Authorization.js'; |
@@ -0,2 +1,3 @@ | ||
export { OAuth2AuthorizationElement } from './src/OAuth2AuthorizationElement.js'; | ||
export { OAuth1AuthorizationElement } from './src/OAuth1AuthorizationElement.js'; | ||
export { OAuth2Authorization } from './src/OAuth2Authorization.js'; | ||
export { OAuth1Authorization } from './src/OAuth1Authorization.js'; |
@@ -1,16 +0,6 @@ | ||
/** | ||
* DO NOT EDIT | ||
* | ||
* This file was automatically generated by | ||
* https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations | ||
* | ||
* To modify these typings, edit the source file(s): | ||
* oauth1-authorization.js | ||
*/ | ||
// tslint:disable:variable-name Describing an API that's defined elsewhere. | ||
import {OAuth1Authorization} from './src/OAuth1Authorization.js'; | ||
export {OAuth1Authorization}; | ||
import {OAuth1AuthorizationElement} from './src/OAuth1AuthorizationElement'; | ||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"oauth1-authorization": OAuth1AuthorizationElement; | ||
} | ||
} |
@@ -14,4 +14,4 @@ /** | ||
*/ | ||
import { OAuth1Authorization } from './src/OAuth1Authorization.js'; | ||
export { OAuth1Authorization }; | ||
window.customElements.define('oauth1-authorization', OAuth1Authorization); | ||
import { OAuth1AuthorizationElement } from './src/OAuth1AuthorizationElement.js'; | ||
window.customElements.define('oauth1-authorization', OAuth1AuthorizationElement); |
@@ -1,16 +0,7 @@ | ||
/** | ||
* DO NOT EDIT | ||
* | ||
* This file was automatically generated by | ||
* https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations | ||
* | ||
* To modify these typings, edit the source file(s): | ||
* oauth2-authorization.js | ||
*/ | ||
import {OAuth2AuthorizationElement} from './src/OAuth2AuthorizationElement.js'; | ||
// tslint:disable:variable-name Describing an API that's defined elsewhere. | ||
import {OAuth2Authorization} from './src/OAuth2Authorization.js'; | ||
export {OAuth2Authorization}; | ||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"oauth2-authorization": OAuth2AuthorizationElement; | ||
} | ||
} |
@@ -14,4 +14,4 @@ /** | ||
*/ | ||
import { OAuth2Authorization } from './src/OAuth2Authorization.js'; | ||
export { OAuth2Authorization }; | ||
window.customElements.define('oauth2-authorization', OAuth2Authorization); | ||
import { OAuth2AuthorizationElement } from './src/OAuth2AuthorizationElement.js'; | ||
window.customElements.define('oauth2-authorization', OAuth2AuthorizationElement); |
100
package.json
{ | ||
"name": "@advanced-rest-client/oauth-authorization", | ||
"description": "A set of elements that perform oauth authorization", | ||
"version": "4.0.3", | ||
"version": "5.0.0", | ||
"license": "Apache-2.0", | ||
"main": "outh-authorization.js", | ||
"main": "index.js", | ||
"module": "index.js", | ||
"keywords": [ | ||
@@ -14,8 +15,6 @@ "web-components", | ||
"authors": [ | ||
"Pawel Psztyc", | ||
"The Advanced REST client authors <arc@mulesoft.com>" | ||
"Pawel Psztyc" | ||
], | ||
"contributors": [ | ||
"Pawel Psztyc", | ||
"The Advanced REST client authors <arc@mulesoft.com>" | ||
"Your name can be here!" | ||
], | ||
@@ -31,42 +30,71 @@ "repository": { | ||
"dependencies": { | ||
"@advanced-rest-client/events-target-mixin": "^3.2.0", | ||
"@advanced-rest-client/arc-events": "^0.2.3", | ||
"@advanced-rest-client/arc-types": "^0.2.14", | ||
"@advanced-rest-client/events-target-mixin": "^3.2.3", | ||
"@advanced-rest-client/headers-parser-mixin": "^3.2.0", | ||
"@polymer/iron-meta": "^3.0.0", | ||
"lit-element": "^2.3.1" | ||
"lit-element": "^2.4.0" | ||
}, | ||
"peerDependencies": { | ||
"cryptojslib": "^3.1.2", | ||
"jsrsasign": "^10.0.5" | ||
}, | ||
"devDependencies": { | ||
"@advanced-rest-client/arc-data-export": "^3.3.2", | ||
"@advanced-rest-client/arc-demo-helper": "^1.0.17", | ||
"@advanced-rest-client/eslint-config": "^1.1.5", | ||
"@advanced-rest-client/prettier-config": "^0.1.0", | ||
"@advanced-rest-client/testing-karma-sl": "^1.3.1", | ||
"@commitlint/cli": "^8.3.5", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@open-wc/testing": "^2.5.16", | ||
"@open-wc/testing-karma": "^3.3.28", | ||
"@polymer/gen-typescript-declarations": "^1.6.2", | ||
"@advanced-rest-client/arc-demo-helper": "^2.2.0", | ||
"@esm-bundle/chai": "^4.1.5", | ||
"@open-wc/eslint-config": "^4.0.1", | ||
"@open-wc/testing": "^2.5.32", | ||
"@web/test-runner": "^0.9.7", | ||
"@web/test-runner-playwright": "^0.6.4", | ||
"cryptojslib": "^3.1.2", | ||
"deepmerge": "^4.2.2", | ||
"es-dev-server": "^1.50.6", | ||
"husky": "^4.2.5", | ||
"jsrsasign": "^8.0.15", | ||
"lint-staged": "^10.2.4", | ||
"sinon": "^9.0.2" | ||
"es-dev-server": "^1.57.8", | ||
"eslint": "^7.13.0", | ||
"eslint-config-prettier": "^6.15.0", | ||
"husky": "^4.3.0", | ||
"jsrsasign": "^10.0.5", | ||
"lint-staged": "^10.5.1", | ||
"sinon": "^9.2.1", | ||
"typescript": "^4.0.5", | ||
"typescript-lit-html-plugin": "^0.9.0" | ||
}, | ||
"scripts": { | ||
"update-types": "gen-typescript-declarations --deleteExisting --outDir .", | ||
"start": "es-dev-server --app-index demo/index.html --node-resolve --open --watch", | ||
"start:compatibility": "es-dev-server --app-index demo/index.html --compatibility all --node-resolve --open --watch", | ||
"lint:eslint": "eslint --ext .js,.html .", | ||
"format:eslint": "eslint --ext .js,.html . --fix", | ||
"lint:prettier": "prettier \"**/*.js\" --list-different || (echo '↑↑ these files are not prettier formatted ↑↑' && exit 1)", | ||
"format:prettier": "prettier \"**/*.js\" --write", | ||
"lint": "npm run lint:eslint && npm run lint:prettier", | ||
"format": "npm run format:eslint && npm run format:prettier", | ||
"test": "karma start --coverage", | ||
"test:watch": "karma start --auto-watch=true --single-run=false", | ||
"test:update-snapshots": "karma start --update-snapshots", | ||
"test:prune-snapshots": "karma start --prune-snapshots", | ||
"test:sl": "karma start karma.sl.config.js --coverage" | ||
"lint:eslint": "eslint --ext .js,.html . --ignore-path .gitignore", | ||
"format:eslint": "eslint --ext .js,.html . --fix --ignore-path .gitignore", | ||
"lint:types": "tsc", | ||
"lint": "npm run lint:eslint", | ||
"format": "npm run format:eslint", | ||
"test": "web-test-runner --coverage --playwright --browsers chromium firefox webkit", | ||
"test:watch": "web-test-runner --watch", | ||
"test:sl": "karma start karma.sl.config.js --compatibility auto --coverage", | ||
"gen:wc": "wca analyze \"*.js\" --outFile custom-elements.json" | ||
}, | ||
"eslintConfig": { | ||
"extends": [ | ||
"@open-wc/eslint-config", | ||
"eslint-config-prettier" | ||
], | ||
"overrides": [ | ||
{ | ||
"files": [ | ||
"**/demo/**/*.js", | ||
"**/test/**/*.js", | ||
"**/demo/**/*.html" | ||
], | ||
"rules": { | ||
"no-console": "off", | ||
"no-plusplus": "off", | ||
"no-unused-expressions": "off", | ||
"class-methods-use-this": "off", | ||
"import/no-extraneous-dependencies": "off" | ||
} | ||
} | ||
] | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged" | ||
} | ||
}, | ||
"lint-staged": { | ||
@@ -73,0 +101,0 @@ "*.js": [ |
188
README.md
@@ -1,136 +0,67 @@ | ||
[![Published on NPM](https://img.shields.io/npm/v/@advanced-rest-client/oauth-authorization.svg)](https://www.npmjs.com/package/@advanced-rest-client/oauth-authorization) | ||
# OAuth authorization | ||
[![Build Status](https://travis-ci.org/advanced-rest-client/api-url-data-model.svg?branch=stage)](https://travis-ci.org/advanced-rest-client/oauth-authorization) | ||
Contains a library and custom elements to perform OAuth2 authorization. | ||
The main library is the `OAuth2Authorization` class that has the logic to perform OAuth 2 authorization. The `<oauth2-authorization>` element is used only to handle the DOM event to start the authorization flow. | ||
[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/advanced-rest-client/oauth-authorization) | ||
## API change | ||
In version 5.0.0 the API surface has changed. The custom element for OAuth 2 authorization only handles the authorization event and uns the authorize function on the `OAuth2Authorization` class. | ||
Also configuration has been redefined and declared in its own TS declaration files. | ||
# OAuth authorization | ||
## Types | ||
Provides components to authorize the user using OAuth 1 and OAuth 2 standards. | ||
Types are defined in the `@advanced-rest-client/arc-types` package. | ||
## OAuth 2 | ||
## Usage | ||
There are 4 basic token requests flows: | ||
### Installation | ||
- Authorization Code for apps running on a web server (`authorization_code` type) | ||
- Implicit for browser-based or mobile apps (`implicit` type) | ||
- Password for logging in with a username and password (`password` type) | ||
- Client credentials for application access (`client_credentials` type) | ||
```sh | ||
npm install --save @advanced-rest-client/oauth-authorization | ||
``` | ||
Additionally you can use custom flow type. | ||
Main function is the `authorize()` function that can be also used via event system. | ||
This function accepts different set of parameters depending on request type. | ||
### Example | ||
```html | ||
<outh2-authorization></outh2-authorization> | ||
``` | ||
```javascript | ||
import { OAuth2Authorization } from '@advanced-rest-client/oauth-authorization'; | ||
```javascript | ||
const settings = { | ||
type: 'implicit', | ||
grantType: 'implicit', | ||
clientId: 'CLIENT ID', | ||
redirectUri: 'https://example.com/auth-popup.html', | ||
authorizationUri: 'https://auth.example.com/token' | ||
authorizationUri: 'https://auth.example.com/token', | ||
scopes: ['email'], | ||
state: 'Optional string' | ||
state: 'Optional string', | ||
}; | ||
const factory = document.querySelector('outh2-authorization'); | ||
factory.authorize(settings) | ||
// or event based | ||
const event = new CustomEvent('oauth2-token-requested', { 'detail': settings, bubbles: true }); | ||
document.dispatchEvent(event); | ||
const factory = new OAuth2Authorization(settings); | ||
const tokenInfo = await factory.authorize(settings) | ||
``` | ||
Listen for token response: | ||
### Popup in authorization flow | ||
```javascript | ||
// oauth2-token-response | ||
factory.ontokenresponse = (e) => { | ||
console.log(e.detial); | ||
}; | ||
// oauth2-error event | ||
factory.ontokenerror = (e) => { | ||
console.log(e.detial); | ||
}; | ||
``` | ||
This package contains the `oauth-popup.html` that can be used to exchange token / code data with hosting page. Other page can be used as well. | ||
The popup page must use the `window.postMessage()` to report back to the library the parameters returned by the authorization server. It expect to return the part of the URL that contains the authorization result. | ||
For example, for the popup url having values like this: `https://redirect.domain.com/popup.html#code=1234&state=5678` the popup window should post message with `code=1234&state=5678`. | ||
An element or app that requesting the token should observe the `oauth2-token-response` and | ||
`oauth2-error` events to get back the response. | ||
### The state parameter and security | ||
## Popup in authorization flow | ||
This element is intend to be used in debug applications where confidentially is already compromised because users may be asked to provide client secret parameter (depending on the flow). | ||
**It should not be used in client applications** that don't serve debugging purposes. Client secret should never be used on the client side. | ||
This element contain a `oauth-popup.html` that can be used to exchange token / code data with | ||
hosting page. Other page can be used as well. But in must `window.postMessage` back to the | ||
`window.opener`. The structure of the message if the parsed query or has string to the map | ||
of parameters. Furthermore it must camel case the parameters. Example script is source code | ||
of the `oauth-popup.html` page. | ||
Popup should be served over the SSL. | ||
To have at least minimum of protection (in already compromised environment) this library generates a `state` parameter as a series of alphanumeric characters and append them to the request. | ||
It is expected to return the same string in the response (as defined in rfc6749). Though this parameter is optional, it will reject the response if the `state` parameter is not the same as the one generated before the request. | ||
## The state parameter and security | ||
The state parameter is generated automatically by the element if non provided in settings. It is a good idea to use this property to check if the event response (either token or error) are coming from your request for token. The app can | ||
support different OAuth clients so you can check later with the token response if this is a response for the same client. | ||
This element is intened to be used in debug applications where confidentialy is already | ||
compromised because users may be asked to provide client secret parameter (depending on the flow). | ||
**It should not be used in client applications** that don't serve debugging purposes. | ||
Client secret should never be used on the client side. | ||
### Non-interactive authorization | ||
To have at least minimum of protection (in already compromised environment) this library generates | ||
a `state` parameter as a series of alphanumeric characters and append them to the request. | ||
It is expected to return the same string in the response (as defined in rfc6749). Though this | ||
parameter is optional, it will reject the response if the `state` parameter is not the same as the | ||
one generated before the request. | ||
For `implicit` and `authorization_code` token requests you can set the `interactive` configuration property to `false` to request the token in the background without rendering any UI related to authorization to the user. | ||
It can be used to request an access token after the user authorized the application. Server should return the token which will be passed back to the application. | ||
The state parameter is generated automatically by the element if non provided in | ||
settings. It is a good idea to use this property to check if the event response | ||
(either token or error) are coming from your request for token. The app can | ||
support different OAuth clients so you can check later with the token response if | ||
this is a response for the same client. | ||
### OAuth 1 | ||
## Non-interactive authorization | ||
The OAuth1 element is deprecated and no longer maintained. The tests for the element has been removed. After consulting with other teams at MuleSoft this element will be removed. | ||
For `implicit` and `code` token requests you can set `interactive` property | ||
of the settings object to `false` to request the token in the background without | ||
displaying any UI related to authorization to the user. | ||
It can be used to request an access token after the user authorized the application. | ||
Server should return the token which will be passed back to the application. | ||
When using `interactive = false` mode then the response event is always | ||
`oauth2-token-response`, even when there was authorization error or user never | ||
authorized the application. In this case the response object will not carry | ||
`accessToken` property and always have `interactive` set to `false` and `code` | ||
to determine cause of unsuccessful request. | ||
### Example | ||
```javascript | ||
const settings = { | ||
interactive: false, | ||
type: 'implicit', | ||
clientId: 'CLIENT ID', | ||
redirectUri: 'https://example.com/auth-popup.html', | ||
authorizationUri: 'https://auth.example.com/token' | ||
state: '1234' | ||
}; | ||
const event = new CustomEvent('oauth2-token-requested', { 'detail': settings, bubbles: true }); | ||
document.dispatchEvent(event); | ||
document.body.addEventListener('oauth2-token-response', (e) => { | ||
let info = e.detail; | ||
if (info.state !== '1234') { | ||
return; | ||
} | ||
if (info.interactive === false && info.code) { | ||
// unsuccessful request | ||
return; | ||
} | ||
let token = info.accessToken; | ||
}); | ||
``` | ||
## OAuth 1 | ||
An element to perform OAuth1 authorization and to sign auth requests. | ||
@@ -151,3 +82,3 @@ | ||
## OAuth 1 configuration object | ||
### OAuth 1 configuration object | ||
@@ -178,43 +109,16 @@ Both authorization or request signing requires detailed configuration object. | ||
## Error codes | ||
### 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. | ||
- `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 | ||
### 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. | ||
- 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. | ||
## Usage | ||
## Development | ||
### Installation | ||
``` | ||
npm install --save @advanced-rest-client/oauth-authorization | ||
``` | ||
### In an html file | ||
```html | ||
<html> | ||
<head> | ||
<script type="module"> | ||
import '@advanced-rest-client/advanced-rest-client/oauth1-authorization.js'; | ||
import '@advanced-rest-client/advanced-rest-client/oauth2-authorization.js'; | ||
</script> | ||
</head> | ||
<body> | ||
<oauth1-authorization></oauth1-authorization> | ||
<oauth2-authorization></oauth2-authorization> | ||
</body> | ||
</html> | ||
``` | ||
### Development | ||
```sh | ||
@@ -221,0 +125,0 @@ git clone https://github.com/advanced-rest-client/oauth-authorization |
@@ -1,377 +0,312 @@ | ||
/** | ||
* DO NOT EDIT | ||
* | ||
* This file was automatically generated by | ||
* https://github.com/Polymer/tools/tree/master/packages/gen-typescript-declarations | ||
* | ||
* To modify these typings, edit the source file(s): | ||
* src/OAuth2Authorization.js | ||
*/ | ||
import { Authorization } from '@advanced-rest-client/arc-types'; | ||
import { ProcessingOptions } from './types'; | ||
export declare const resolveFunction: unique symbol; | ||
export declare const rejectFunction: unique symbol; | ||
export declare const settingsValue: unique symbol; | ||
export declare const optionsValue: unique symbol; | ||
export declare const prepareSettings: unique symbol; | ||
export declare const prepareOptions: unique symbol; | ||
export declare const authorize: unique symbol; | ||
export declare const stateValue: unique symbol; | ||
export declare const authorizeImplicitCode: unique symbol; | ||
export declare const authorizeClientCredentials: unique symbol; | ||
export declare const authorizePassword: unique symbol; | ||
export declare const authorizeCustomGrant: unique symbol; | ||
export declare const popupValue: unique symbol; | ||
export declare const popupUnloadHandler: unique symbol; | ||
export declare const tokenResponse: unique symbol; | ||
export declare const messageHandler: unique symbol; | ||
export declare const iframeValue: unique symbol; | ||
export declare const processPopupRawData: unique symbol; | ||
export declare const processTokenResponse: unique symbol; | ||
export declare const handleTokenInfo: unique symbol; | ||
export declare const computeTokenInfoScopes: unique symbol; | ||
export declare const computeExpires: unique symbol; | ||
export declare const codeValue: unique symbol; | ||
export declare const frameTimeoutHandler: unique symbol; | ||
export declare const reportOAuthError: unique symbol; | ||
export declare const authorizePopup: unique symbol; | ||
export declare const authorizeTokenNonInteractive: unique symbol; | ||
export declare const createTokenResponseError: unique symbol; | ||
export declare const createErrorParams: unique symbol; | ||
export declare const tokenInfoFromParams: unique symbol; | ||
export declare const processCodeResponse: unique symbol; | ||
export declare const handleTokenCodeError: unique symbol; | ||
export declare const codeVerifierValue: unique symbol; | ||
// 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 {EventsTargetMixin} from '@advanced-rest-client/events-target-mixin/events-target-mixin.js'; | ||
export {OAuth2Authorization}; | ||
/** | ||
* The `<outh2-authorization>` performs an OAuth2 requests to get a token for given settings. | ||
* A library that performs OAuth 2 authorization. | ||
* | ||
* It is build for API components ecosystem and the configuration is defined in `@advanced-rest-client/arc-types` | ||
* so all components use the same configuration. | ||
*/ | ||
declare class OAuth2Authorization extends | ||
EventsTargetMixin( | ||
Object) { | ||
readonly tokenInfo: object|null; | ||
ontokenerror: Function|null; | ||
ontokenresponse: Function|null; | ||
_attachListeners(node: any): void; | ||
_detachListeners(node: any): void; | ||
export class OAuth2Authorization { | ||
/** | ||
* Clears the state of the element. | ||
* The authorization settings used to initialize this class. | ||
*/ | ||
clear(): void; | ||
get settings(): Authorization.OAuth2Authorization; | ||
[settingsValue]: Authorization.OAuth2Authorization; | ||
/** | ||
* Clean up popup reference and closes the window if not yet closed. | ||
* The processing options used to initialize this object. | ||
*/ | ||
_cleanupPopup(): void; | ||
get options(): ProcessingOptions; | ||
[optionsValue]: ProcessingOptions; | ||
/** | ||
* Handler for the `oauth2-token-requested` custom event. | ||
* The request state parameter. If the state is not passed with the configuration one is generated. | ||
*/ | ||
_tokenRequestedHandler(e: CustomEvent|null): void; | ||
get state(): string; | ||
[stateValue]: string; | ||
/** | ||
* 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. | ||
* | ||
* 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: {[key: String|null]: String|null}): void; | ||
[resolveFunction]: Function; | ||
[rejectFunction]: Function; | ||
/** | ||
* 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; | ||
[codeVerifierValue]: string; | ||
/** | ||
* Checks if the URL has valid scheme for OAuth flow. | ||
* | ||
* @param url The url value to test | ||
* @param settings The authorization configuration. | ||
* @param options Additional processing options to configure the behavior of this library. | ||
*/ | ||
_checkUrl(url: String|null): void; | ||
constructor(settings: Authorization.OAuth2Authorization, options?: ProcessingOptions); | ||
/** | ||
* 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 | ||
* @returns Processed settings | ||
*/ | ||
_authorize(authUrl: String|null, settings: object|null): void; | ||
[prepareSettings](settings: Authorization.OAuth2Authorization): Authorization.OAuth2Authorization; | ||
/** | ||
* Creates and opens auth popup. | ||
* | ||
* @param url Complete authorization url | ||
* @returns Processed options | ||
*/ | ||
_authorizePopup(url: String|null): void; | ||
[prepareOptions](options: ProcessingOptions): ProcessingOptions; | ||
/** | ||
* 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 | ||
* A function that should be called before the authorization. | ||
* It checks configuration integrity, and performs some sanity checks | ||
* like proper values of the request URIs. | ||
*/ | ||
_authorizeTokenNonInteractive(url: String|null): void; | ||
checkConfig(): void; | ||
/** | ||
* Removes the frame and any event listeners attached to it. | ||
* Performs the authorization. | ||
* @returns Promise resolved to the token info. | ||
*/ | ||
_cleanupFrame(): void; | ||
authorize(): Promise<Authorization.TokenInfo>; | ||
/** | ||
* Handler for `error` event dispatched by oauth iframe. | ||
* Reports authorization error back to the application. | ||
* | ||
* This operation clears the promise object. | ||
* | ||
* @param message The message to report | ||
* @param code Error code | ||
*/ | ||
_frameLoadErrorHandler(): void; | ||
[reportOAuthError](message: string, code: string): void; | ||
/** | ||
* Handler for iframe `load` event. | ||
* Starts the authorization process. | ||
*/ | ||
_frameLoadHandler(): void; | ||
[authorize](): void; | ||
/** | ||
* Observer if the popup has been closed befor the data has been received. | ||
* Starts the authorization flow for the `implicit` and `authorization_code` flows. | ||
* If the `interactive` flag is configured it then it chooses between showing the UI (popup) | ||
* or non-interactive iframe. | ||
*/ | ||
_observePopupState(): void; | ||
[authorizeImplicitCode](): Promise<void>; | ||
/** | ||
* Function called in the interval. | ||
* Observer popup state and calls `_beforePopupUnloadHandler()` | ||
* when popup is no longer opened. | ||
* Constructs the popup/iframe URL for the `implicit` or `authorization_code` grant types. | ||
* @return Full URL for the endpoint. | ||
*/ | ||
_popupObserver(): void; | ||
constructPopupUrl(): Promise<string>; | ||
/** | ||
* 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. | ||
* Opens a popup to request authorization from the user. | ||
* @param url The URL to open. | ||
*/ | ||
_constructPopupUrl(settings: object|null, type: String|null): String|null; | ||
[authorizePopup](url: string): void; | ||
/** | ||
* Computes `scope` URL parameter from scopes array. | ||
* Tries to authorize the user in a non interactive way (iframe rather than a popup). | ||
* | ||
* 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 scopes List of scopes to use with the request. | ||
* @returns Computed scope value. | ||
* @param url Complete authorization url | ||
*/ | ||
_computeScope(scopes: Array<String|null>|null): String|null; | ||
[authorizeTokenNonInteractive](url: string): void; | ||
/** | ||
* Listens for a message from the popup. | ||
* Event handler for the the iframe timeout event. | ||
* If there's the reject function then it is called with the error details. | ||
*/ | ||
_popupMessageHandler(e: Event|null): void; | ||
_processPopupData(e: any): void; | ||
_clearIframeTimeout(): void; | ||
[frameTimeoutHandler](): void; | ||
/** | ||
* http://stackoverflow.com/a/10727155/1127848 | ||
* Clears all registered observers: | ||
* - popup/iframe message listeners | ||
* - popup info pull interval | ||
*/ | ||
randomString(len: any): any; | ||
clearObservers(): void; | ||
/** | ||
* 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. | ||
* This is called when the popup info pull interval detects that the window was closed. | ||
* It checks whether the token info has been set by the redirect page and if not then it reports an error. | ||
*/ | ||
_beforePopupUnloadHandler(): void; | ||
[popupUnloadHandler](): 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. | ||
* A handler for the `message` event registered when performing authorization that involves the popup | ||
* of the iframe. | ||
*/ | ||
_exchangeCode(code: String|null): Promise<any>|null; | ||
[messageHandler](e: MessageEvent): void; | ||
/** | ||
* 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. | ||
* @param raw The data from the `MessageEvent`. Might not be the data returned by the auth popup/iframe. | ||
*/ | ||
_getCodeEchangeBody(settings: object|null, code: String|null): String|null; | ||
[processPopupRawData](raw: any): void; | ||
/** | ||
* 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. | ||
* Processes the response returned by the popup or the iframe. | ||
* @param oauthParams | ||
*/ | ||
_requestToken(url: String|null, body: String|null, settings: object|null): Promise<any>|null; | ||
[processTokenResponse](oauthParams: URLSearchParams): Promise<void>; | ||
/** | ||
* 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 | ||
* Processes the response returned by the popup or the iframe. | ||
* @param oauthParams | ||
* @returns Parameters for the [reportOAuthError]() function | ||
*/ | ||
_processTokenResponseHandler(e: Event|null, resolve: Function|null, reject: Function|null): void; | ||
[createTokenResponseError](oauthParams: URLSearchParams): string[]; | ||
/** | ||
* Handler for the code request error event. | ||
* Rejects the promise with error description. | ||
* | ||
* @param e XHR error event | ||
* @param reject Promise's reject function. | ||
* Creates arguments for the error function from error response | ||
* @param code Returned from the authorization server error code | ||
* @param description Returned from the authorization server error description | ||
* @returns Parameters for the [reportOAuthError]() function | ||
*/ | ||
_processTokenResponseErrorHandler(e: Event|null, reject: Function|null): void; | ||
[createErrorParams](code: string, description?: string): string[]; | ||
/** | ||
* 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. | ||
* Creates a token info object from query parameters | ||
*/ | ||
_processCodeResponse(body: String|null, contentType: String|null): object|null; | ||
[tokenInfoFromParams](oauthParams: URLSearchParams): Authorization.TokenInfo; | ||
/** | ||
* 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. | ||
* @param info Token info returned from the server. | ||
*/ | ||
_handleTokenInfo(tokenInfo: object|null): object|null; | ||
[handleTokenInfo](info: Authorization.TokenInfo): void; | ||
/** | ||
* Handler fore an error that happened during code exchange. | ||
*/ | ||
_handleTokenCodeError(e: Error|null): void; | ||
/** | ||
* Replaces `-` or `_` with camel case. | ||
* Computes token expiration time. | ||
* It sets `expires_at` property on the token info object which is the time | ||
* in the future when when the token expires. | ||
* | ||
* @param name The string to process | ||
* @returns Camel cased string or `undefined` if not | ||
* transformed. | ||
* @param tokenInfo Token info object | ||
* @returns A copy with updated properties. | ||
*/ | ||
_camel(name: String|null): String|null|undefined; | ||
[computeExpires](tokenInfo: Authorization.TokenInfo): Authorization.TokenInfo; | ||
/** | ||
* Requests a token for `password` request type. | ||
* Computes the final list of granted scopes. | ||
* It is a list of scopes received in the response or the list of requested scopes. | ||
* Because the user may change the list of scopes during the authorization process | ||
* the received list of scopes can be different than the one requested by the user. | ||
* | ||
* @param settings The same settings as passed to `authorize()` | ||
* function. | ||
* @returns Promise resolved to token info. | ||
* @param scope The `scope` parameter received with the response. It's null safe. | ||
* @returns The list of scopes for the token. | ||
*/ | ||
authorizePassword(settings: object|null): Promise<any>|null; | ||
[computeTokenInfoScopes](scope: string): string[]; | ||
/** | ||
* Generates a payload message for password authorization. | ||
* Exchanges the authorization code for authorization token. | ||
* | ||
* @param settings Settings object passed to the `authorize()` | ||
* function | ||
* @returns Message body as defined in OAuth2 spec. | ||
* @param code Returned code from the authorization endpoint. | ||
* @returns The token info when the request was a success. | ||
*/ | ||
_getPasswordBody(settings: object|null): String|null; | ||
exchangeCode(code: string): Promise<Authorization.TokenInfo>; | ||
/** | ||
* 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. | ||
* Returns a body value for the code exchange request. | ||
* @param code Authorization code value returned by the authorization server. | ||
* @return Request body. | ||
*/ | ||
authorizeClientCredentials(settings: object|null): Promise<any>|null; | ||
getCodeRequestBody(code: string): string; | ||
/** | ||
* Generates a payload message for client credentials. | ||
* Requests for token from the authorization server for `code`, `password`, `client_credentials` and custom grant types. | ||
* | ||
* @param settings Settings object passed to the `authorize()` | ||
* function | ||
* @returns Message body as defined in OAuth2 spec. | ||
* @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. | ||
* @returns Promise resolved to the response string. | ||
*/ | ||
_getClientCredentialsBody(settings: object|null): String|null; | ||
requestToken(url: string, body: string): Promise<Authorization.TokenInfo>; | ||
/** | ||
* Performs authorization on custom grant type. | ||
* This extension is described in OAuth 2.0 spec. | ||
* Processes code response body and produces map of values. | ||
* | ||
* @param settings Settings object as for `authorize()` function. | ||
* @returns Promise resolved to a token info object. | ||
* @param body Body received in the response. | ||
* @param mime Response content type. | ||
* @return Response as an object. | ||
* @throws {Error} Exception when the body is invalid. | ||
*/ | ||
authorizeCustomGrant(settings: object|null): Promise<any>|null; | ||
[processCodeResponse](body: string, mime: string): Authorization.TokenInfo; | ||
/** | ||
* 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. | ||
* A handler for the error that happened during code exchange. | ||
*/ | ||
_getCustomGrantBody(settings: object|null): String|null; | ||
[handleTokenCodeError](e: Error): void; | ||
/** | ||
* Applies custom properties defined in the OAuth settings object to the URL. | ||
* Requests a token for `client_credentials` request type. | ||
* | ||
* This method resolves the main promise set by the `authorize()` function. | ||
* | ||
* @param url Generated URL for an endpoint. | ||
* @param data `customData.[type]` property from the settings object. | ||
* The type is either `auth` or `token`. | ||
* @return Promise resolved to a token info object. | ||
*/ | ||
_applyCustomSettingsQuery(url: String|null, data: object|null): String|null; | ||
[authorizeClientCredentials](): Promise<void>; | ||
/** | ||
* Applies custom headers from the settings object | ||
* Generates a payload message for client credentials. | ||
* | ||
* @param xhr Instance of the request object. | ||
* @param data Value of settings' `customData` property | ||
* @return Message body as defined in OAuth2 spec. | ||
*/ | ||
_applyCustomSettingsHeaders(xhr: XMLHttpRequest|null, data: object|null): void; | ||
getClientCredentialsBody(): string; | ||
/** | ||
* Applies custom body properties from the settings to the body value. | ||
* Requests a token for `client_credentials` request type. | ||
* | ||
* This method resolves the main promise set by the `authorize()` function. | ||
* | ||
* @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 | ||
* @returns Promise resolved to a token info object. | ||
*/ | ||
_applyCustomSettingsBody(body: String|null, data: object|null): String|null; | ||
[authorizePassword](): Promise<void>; | ||
/** | ||
* Dispatches an error event that propagates through the DOM. | ||
* Generates a payload message for password authorization. | ||
* | ||
* @param detail The detail object. | ||
* @return Message body as defined in OAuth2 spec. | ||
*/ | ||
_dispatchError(detail: object|null): void; | ||
getPasswordBody(): string; | ||
/** | ||
* Dispatches an event with the authorization code that propagates through the DOM. | ||
* Closes the popup once the authorization code has been dispatched. | ||
* Performs authorization on custom grant type. | ||
* This extension is described in OAuth 2.0 spec. | ||
* | ||
* This method resolves the main promise set by the `authorize()` function. | ||
* | ||
* @param detail The detail object. | ||
* @returns Promise resolved when the request finish. | ||
*/ | ||
_dispatchCodeResponse(detail: object|null): void; | ||
[authorizeCustomGrant](): Promise<void>; | ||
/** | ||
* Dispatches an event with the token (e.g. access token) that propagates through the DOM. | ||
* Generates a payload message for the custom grant. | ||
* | ||
* @param detail The detail object. | ||
* @return {string} Message body as defined in OAuth2 spec. | ||
*/ | ||
_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; | ||
} | ||
getCustomGrantBody(): string; | ||
} |
@@ -1,984 +0,847 @@ | ||
import { EventsTargetMixin } from '@advanced-rest-client/events-target-mixin/events-target-mixin.js'; | ||
/* eslint-disable no-param-reassign */ | ||
/* eslint-disable class-methods-use-this */ | ||
import { sanityCheck, randomString, camel, generateCodeChallenge } from './Utils.js'; | ||
import { applyCustomSettingsQuery, applyCustomSettingsBody, applyCustomSettingsHeaders } from './CustomParameters.js'; | ||
import { AuthorizationError, CodeError } from './AuthorizationError.js'; | ||
import { IframeAuthorization } from './lib/IframeAuthorization.js'; | ||
import { PopupAuthorization } from './lib/PopupAuthorization.js'; | ||
/** @typedef {import('@advanced-rest-client/arc-types').Authorization.OAuth2Authorization} OAuth2Settings */ | ||
/** @typedef {import('@advanced-rest-client/arc-types').Authorization.TokenInfo} TokenInfo */ | ||
/** @typedef {import('./types').ProcessingOptions} ProcessingOptions */ | ||
export const resolveFunction = Symbol('resolveFunction'); | ||
export const rejectFunction = Symbol('rejectFunction'); | ||
export const settingsValue = Symbol('settingsValue'); | ||
export const optionsValue = Symbol('optionsValue'); | ||
export const prepareSettings = Symbol('prepareSettings'); | ||
export const prepareOptions = Symbol('prepareOptions'); | ||
export const authorize = Symbol('authorize'); | ||
export const stateValue = Symbol('stateValue'); | ||
export const authorizeImplicitCode = Symbol('authorizeImplicitCode'); | ||
export const authorizeClientCredentials = Symbol('authorizeClientCredentials'); | ||
export const authorizePassword = Symbol('authorizePassword'); | ||
export const authorizeCustomGrant = Symbol('authorizeCustomGrant'); | ||
export const popupValue = Symbol('popupValue'); | ||
export const popupUnloadHandler = Symbol('popupUnloadHandler'); | ||
export const tokenResponse = Symbol('tokenResponse'); | ||
export const messageHandler = Symbol('messageHandler'); | ||
export const iframeValue = Symbol('iframeValue'); | ||
export const processPopupRawData = Symbol('processPopupRawData'); | ||
export const processTokenResponse = Symbol('processTokenResponse'); | ||
export const handleTokenInfo = Symbol('handleTokenInfo'); | ||
export const computeTokenInfoScopes = Symbol('computeTokenInfoScopes'); | ||
export const computeExpires = Symbol('computeExpires'); | ||
export const codeValue = Symbol('codeValue'); | ||
export const frameTimeoutHandler = Symbol('frameTimeoutHandler'); | ||
export const reportOAuthError = Symbol('reportOAuthError'); | ||
export const authorizePopup = Symbol('authorizePopup'); | ||
export const authorizeTokenNonInteractive = Symbol('authorizeTokenNonInteractive'); | ||
export const createTokenResponseError = Symbol('createTokenResponseError'); | ||
export const createErrorParams = Symbol('createErrorParams'); | ||
export const tokenInfoFromParams = Symbol('tokenInfoFromParams'); | ||
export const processCodeResponse = Symbol('processCodeResponse'); | ||
export const handleTokenCodeError = Symbol('handleTokenCodeError'); | ||
export const codeVerifierValue = Symbol('codeVerifierValue'); | ||
/** | ||
The `<outh2-authorization>` performs an OAuth2 requests to get a token for given settings. | ||
* A library that performs OAuth 2 authorization. | ||
* | ||
* It is build for API components ecosystem and the configuration is defined in `@advanced-rest-client/arc-types` | ||
* so all components use the same configuration. | ||
*/ | ||
export class OAuth2Authorization { | ||
/** | ||
* @returns {OAuth2Settings} The authorization settings used to initialize this class. | ||
*/ | ||
get settings() { | ||
return this[settingsValue]; | ||
} | ||
@customElement | ||
@appliesMixin EventsTargetMixin | ||
*/ | ||
export class OAuth2Authorization extends EventsTargetMixin(HTMLElement) { | ||
/** | ||
* @return {Object} A full data returned by the authorization endpoint. | ||
* @returns {ProcessingOptions} The processing options used to initialize this object. | ||
*/ | ||
get tokenInfo() { | ||
return this._tokenInfo; | ||
get options() { | ||
return this[optionsValue]; | ||
} | ||
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); | ||
/** | ||
* @returns {string} The request state parameter. If the state is not passed with the configuration one is generated. | ||
*/ | ||
get state() { | ||
if (!this[stateValue]) { | ||
this[stateValue] = this.settings.state || randomString(); | ||
} | ||
return this[stateValue]; | ||
} | ||
_attachListeners(node) { | ||
node.addEventListener('oauth2-token-requested', this._tokenRequestedHandler); | ||
window.addEventListener('message', this._popupMessageHandler); | ||
this.setAttribute('aria-hidden', 'true'); | ||
/** | ||
* @param {OAuth2Settings} settings The authorization configuration. | ||
* @param {ProcessingOptions=} options Additional processing options to configure the behavior of this library. | ||
*/ | ||
constructor(settings, options={}) { | ||
if (!settings) { | ||
throw new TypeError('Expected one argument.'); | ||
} | ||
/** | ||
* @type {Function} The main resolve function | ||
*/ | ||
this[resolveFunction] = undefined; | ||
/** | ||
* @type {Function} The main reject function | ||
*/ | ||
this[rejectFunction] = undefined; | ||
/** | ||
* @type {OAuth2Settings} The authorization settings | ||
*/ | ||
this[settingsValue] = this[prepareSettings](settings); | ||
/** | ||
* @type {ProcessingOptions} The processing options. | ||
*/ | ||
this[optionsValue] = this[prepareOptions](options); | ||
this[messageHandler] = this[messageHandler].bind(this); | ||
this[frameTimeoutHandler] = this[frameTimeoutHandler].bind(this); | ||
this[popupUnloadHandler] = this[popupUnloadHandler].bind(this); | ||
} | ||
_detachListeners(node) { | ||
node.removeEventListener('oauth2-token-requested', this._tokenRequestedHandler); | ||
window.removeEventListener('message', this._popupMessageHandler); | ||
/** | ||
* @param {OAuth2Settings} settings | ||
* @returns {OAuth2Settings} Processed settings | ||
*/ | ||
[prepareSettings](settings) { | ||
const copy = { ...settings }; | ||
Object.freeze(copy); | ||
return copy; | ||
} | ||
/** | ||
* Clears the state of the element. | ||
* @param {ProcessingOptions} options | ||
* @returns {ProcessingOptions} Processed options | ||
*/ | ||
clear() { | ||
this._state = undefined; | ||
this._settings = undefined; | ||
this._cleanupFrame(); | ||
this._cleanupPopup(); | ||
[prepareOptions](options) { | ||
const copy = { | ||
popupPullTimeout: 50, | ||
messageTarget: window, | ||
...options, | ||
}; | ||
Object.freeze(copy); | ||
return copy; | ||
} | ||
/** | ||
* Clean up popup reference and closes the window if not yet closed. | ||
* A function that should be called before the authorization. | ||
* It checks configuration integrity, and performs some sanity checks | ||
* like proper values of the request URIs. | ||
*/ | ||
_cleanupPopup() { | ||
if (this._popup) { | ||
if (!this._popup.closed) { | ||
this._popup.close(); | ||
} | ||
this._popup = undefined; | ||
} | ||
checkConfig() { | ||
// @todo(pawel): perform settings integrity tests. | ||
sanityCheck(this.settings); | ||
} | ||
/** | ||
* Handler for the `oauth2-token-requested` custom event. | ||
* | ||
* @param {CustomEvent} e | ||
* Performs the authorization. | ||
* @returns {Promise<TokenInfo>} Promise resolved to the token info. | ||
*/ | ||
_tokenRequestedHandler(e) { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
e.stopImmediatePropagation(); | ||
this.authorize(e.detail); | ||
authorize() { | ||
return new Promise((resolve, reject) => { | ||
this[resolveFunction] = resolve; | ||
this[rejectFunction] = reject; | ||
this[authorize](); | ||
}); | ||
} | ||
/** | ||
* Authorize the user using provided settings. | ||
* Reports authorization error back to the application. | ||
* | ||
* @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. | ||
* This operation clears the promise object. | ||
* | ||
* 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. | ||
* @param {string} message The message to report | ||
* @param {string} code Error code | ||
*/ | ||
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; | ||
[reportOAuthError](message, code) { | ||
this.clearObservers(); | ||
if (!this[rejectFunction]) { | ||
return; | ||
} | ||
const interactive = typeof this.settings.interactive === 'boolean' ? this.settings.interactive : true; | ||
const e = new AuthorizationError( | ||
message, | ||
code, | ||
this.state, | ||
interactive, | ||
); | ||
this[rejectFunction](e); | ||
this[rejectFunction] = undefined; | ||
this[resolveFunction] = undefined; | ||
} | ||
switch (settings.type) { | ||
/** | ||
* Starts the authorization process. | ||
*/ | ||
[authorize]() { | ||
const { settings } = this; | ||
switch (settings.grantType) { | ||
case 'implicit': | ||
this._authorize(this._constructPopupUrl(settings, 'token'), settings); | ||
break; | ||
case 'authorization_code': | ||
this._authorize(this._constructPopupUrl(settings, 'code'), settings); | ||
this[authorizeImplicitCode](); | ||
break; | ||
case 'client_credentials': | ||
this.authorizeClientCredentials(settings).catch(() => {}); | ||
this[authorizeClientCredentials](); | ||
break; | ||
case 'password': | ||
this.authorizePassword(settings).catch(() => {}); | ||
this[authorizePassword](); | ||
break; | ||
default: | ||
this.authorizeCustomGrant(settings).catch(() => {}); | ||
this[authorizeCustomGrant](); | ||
} | ||
} | ||
/** | ||
* 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 | ||
* Starts the authorization flow for the `implicit` and `authorization_code` flows. | ||
* If the `interactive` flag is configured it then it chooses between showing the UI (popup) | ||
* or non-interactive iframe. | ||
*/ | ||
_sanityCheck(settings) { | ||
if (settings.type === 'implicit' || settings.type === 'authorization_code') { | ||
try { | ||
this._checkUrl(settings.authorizationUri); | ||
} catch (e) { | ||
throw new Error(`authorizationUri: ${e.message}`); | ||
async [authorizeImplicitCode]() { | ||
const { settings } = this; | ||
const url = await this.constructPopupUrl(); | ||
try { | ||
if (settings.interactive === false) { | ||
this[authorizeTokenNonInteractive](url); | ||
} else { | ||
this[authorizePopup](url); | ||
} | ||
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}`); | ||
} | ||
} | ||
this.options.messageTarget.addEventListener('message', this[messageHandler]); | ||
} catch (e) { | ||
this[rejectFunction](e); | ||
this[rejectFunction] = undefined; | ||
this[resolveFunction] = undefined; | ||
} | ||
} | ||
/** | ||
* 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 | ||
* Constructs the popup/iframe URL for the `implicit` or `authorization_code` grant types. | ||
* @return {Promise<string>} Full URL for the endpoint. | ||
*/ | ||
_checkUrl(url) { | ||
if (!url) { | ||
throw new TypeError('the value is missing'); | ||
async constructPopupUrl() { | ||
const { settings } = this; | ||
const mapping = { | ||
implicit: 'token', | ||
authorization_code: 'code', | ||
}; | ||
const type = mapping[settings.grantType]; | ||
if (!type) { | ||
return null; | ||
} | ||
if (typeof url !== 'string') { | ||
throw new TypeError('the value is not a string'); | ||
const url = new URL(settings.authorizationUri); | ||
url.searchParams.set('response_type', type); | ||
url.searchParams.set('client_id', settings.clientId); | ||
url.searchParams.set('state', this.state); | ||
if (settings.redirectUri) { | ||
url.searchParams.set('redirect_uri', settings.redirectUri); | ||
} | ||
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) { | ||
throw new Error('the value has invalid scheme'); | ||
const { scopes } = settings; | ||
if (Array.isArray(scopes) && scopes.length) { | ||
url.searchParams.set('scope', scopes.join(' ')); | ||
} | ||
} | ||
/** | ||
* 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.includeGrantedScopes) { | ||
// this is Google specific | ||
url.searchParams.set('include_granted_scopes', 'true'); | ||
} | ||
if (settings.loginHint) { | ||
// this is Google specific | ||
url.searchParams.set('login_hint', settings.loginHint); | ||
} | ||
if (settings.interactive === false) { | ||
this._authorizeTokenNonInteractive(authUrl); | ||
} else { | ||
this._authorizePopup(authUrl); | ||
// this is Google specific | ||
url.searchParams.set('prompt', 'none'); | ||
} | ||
if (settings.pkce && type === 'code') { | ||
this[codeVerifierValue] = randomString(); | ||
const challenge = await generateCodeChallenge(this[codeVerifierValue]); | ||
url.searchParams.set('code_challenge', challenge); | ||
url.searchParams.set('code_challenge_method', 'S256'); | ||
} | ||
// custom query parameters from the `api-authorization-method` component | ||
if (settings.customData) { | ||
const cs = settings.customData.auth; | ||
if (cs) { | ||
applyCustomSettingsQuery(url, cs); | ||
} | ||
} | ||
return url.toString(); | ||
} | ||
/** | ||
* Creates and opens auth popup. | ||
* | ||
* @param {String} url Complete authorization url | ||
* Opens a popup to request authorization from the user. | ||
* @param {string} url The URL to open. | ||
*/ | ||
_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; | ||
[authorizePopup](url) { | ||
const popup = new PopupAuthorization(this.options.popupPullTimeout); | ||
try { | ||
popup.load(url); | ||
} catch (e) { | ||
throw new AuthorizationError( | ||
e.message, | ||
'popup_blocked', | ||
this.state, | ||
this.settings.interactive, | ||
); | ||
} | ||
this._popup.window.focus(); | ||
this._observePopupState(); | ||
popup.addEventListener('close', this[popupUnloadHandler]); | ||
this[popupValue] = popup; | ||
} | ||
/** | ||
* Tries to Authorize the user in a non interactive way. | ||
* Tries to authorize the user in a non interactive way (iframe rather than a popup). | ||
* | ||
* 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 | ||
* @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; | ||
[authorizeTokenNonInteractive](url) { | ||
const iframe = new IframeAuthorization(this.options.iframeTimeout); | ||
iframe.addEventListener('timeout', this[frameTimeoutHandler]); | ||
iframe.load(url); | ||
this[iframeValue] = iframe; | ||
} | ||
/** | ||
* Removes the frame and any event listeners attached to it. | ||
* Event handler for the the iframe timeout event. | ||
* If there's the reject function then it is called with the error details. | ||
*/ | ||
_cleanupFrame() { | ||
if (!this._iframe) { | ||
[frameTimeoutHandler]() { | ||
if (!this[rejectFunction]) { | ||
return; | ||
} | ||
this._iframe.removeEventListener('error', this._frameLoadErrorHandler); | ||
this._iframe.removeEventListener('load', this._frameLoadHandler); | ||
try { | ||
document.body.removeChild(this._iframe); | ||
} catch (_) { | ||
// ... | ||
const e = new AuthorizationError( | ||
'Non-interactive authorization failed.', | ||
'iframe_load_error', | ||
this.state, | ||
false, | ||
); | ||
this[rejectFunction](e); | ||
this[rejectFunction] = undefined; | ||
this[resolveFunction] = undefined; | ||
} | ||
/** | ||
* Clears all registered observers: | ||
* - popup/iframe message listeners | ||
* - popup info pull interval | ||
*/ | ||
clearObservers() { | ||
this.options.messageTarget.removeEventListener('message', this[messageHandler]); | ||
if (this[popupValue]) { | ||
this[popupValue].cleanUp(); | ||
this[popupValue] = undefined; | ||
} | ||
this._iframe = undefined; | ||
if (this[iframeValue]) { | ||
this[iframeValue].cancel(); | ||
this[iframeValue].cleanUp(); | ||
this[iframeValue] = undefined; | ||
} | ||
} | ||
/** | ||
* Handler for `error` event dispatched by oauth iframe. | ||
* This is called when the popup info pull interval detects that the window was closed. | ||
* It checks whether the token info has been set by the redirect page and if not then it reports an error. | ||
*/ | ||
_frameLoadErrorHandler() { | ||
if (this._errored) { | ||
[popupUnloadHandler]() { | ||
if (this[tokenResponse] || (this.settings.grantType === 'authorization_code' && this[codeValue])) { | ||
// everything seems to be ok. | ||
return; | ||
} | ||
this._dispatchResponse({ | ||
interactive: false, | ||
code: 'iframe_load_error', | ||
state: this._state | ||
}); | ||
this.clear(); | ||
if (!this[rejectFunction]) { | ||
// someone already called it. | ||
return; | ||
} | ||
this[reportOAuthError]('No response has been recorded.', 'no_response'); | ||
} | ||
/** | ||
* Handler for iframe `load` event. | ||
* A handler for the `message` event registered when performing authorization that involves the popup | ||
* of the iframe. | ||
* @param {MessageEvent} e | ||
*/ | ||
_frameLoadHandler() { | ||
if (this.__frameLoadInfo) { | ||
[messageHandler](e) { | ||
const popup = this[popupValue]; | ||
const iframe = this[iframeValue]; | ||
if (!popup && !iframe) { | ||
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); | ||
this[processPopupRawData](e.data); | ||
} | ||
// 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. | ||
* @param {any} raw The data from the `MessageEvent`. Might not be the data returned by the auth popup/iframe. | ||
*/ | ||
_popupObserver() { | ||
if (!this._popup || this._popup.closed) { | ||
clearInterval(this.__popupCheckInterval); | ||
this.__popupCheckInterval = undefined; | ||
this._beforePopupUnloadHandler(); | ||
[processPopupRawData](raw) { | ||
if (!raw) { | ||
return; | ||
} | ||
let params; | ||
try { | ||
params = new URLSearchParams(raw); | ||
} catch (e) { | ||
this[reportOAuthError]('Invalid response from the redirect page'); | ||
return; | ||
} | ||
if (params.has('error') || params.has('access_token') || params.has('code')) { | ||
this[processTokenResponse](params); | ||
} | ||
} | ||
/** | ||
* 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. | ||
* Processes the response returned by the popup or the iframe. | ||
* @param {URLSearchParams} oauthParams | ||
*/ | ||
_constructPopupUrl(settings, type) { | ||
let url = settings.authorizationUri; | ||
if (url.indexOf('?') === -1) { | ||
url += '?'; | ||
} else { | ||
url += '&'; | ||
async [processTokenResponse](oauthParams) { | ||
this.clearObservers(); | ||
const state = oauthParams.get('state'); | ||
if (!state) { | ||
this[reportOAuthError]('Server did not return the state parameter.', 'no_state'); | ||
return; | ||
} | ||
url += 'response_type=' + type; | ||
url += '&client_id=' + encodeURIComponent(settings.clientId || ''); | ||
if (settings.redirectUri) { | ||
url += '&redirect_uri=' + encodeURIComponent(settings.redirectUri); | ||
if (state !== this.state) { | ||
// @todo(@jarrodek): Can it happen to have more than a single token request (opened popups) at the same time? | ||
this[reportOAuthError]('The state value returned by the authorization server is invalid.', 'invalid_state'); | ||
return; | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
url += '&scope=' + this._computeScope(settings.scopes); | ||
if (oauthParams.has('error')) { | ||
this[reportOAuthError](...this[createTokenResponseError](oauthParams)); | ||
return; | ||
} | ||
url += '&state=' + encodeURIComponent(this._state); | ||
if (settings.includeGrantedScopes) { | ||
url += '&include_granted_scopes=true'; | ||
const { grantType } = this.settings; | ||
if (grantType === 'implicit') { | ||
this[handleTokenInfo](this[tokenInfoFromParams](oauthParams)); | ||
return; | ||
} | ||
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); | ||
if (grantType === 'authorization_code') { | ||
const code = oauthParams.get('code'); | ||
if (!code) { | ||
this[reportOAuthError]('The authorization server did not returned the authorization code.', 'no_code'); | ||
return; | ||
} | ||
this[codeValue] = code; | ||
let tokenInfo; | ||
try { | ||
tokenInfo = await this.exchangeCode(code); | ||
} catch (e) { | ||
this[handleTokenCodeError](e); | ||
return; | ||
} | ||
this[handleTokenInfo](tokenInfo); | ||
return; | ||
} | ||
return url; | ||
this[reportOAuthError]('The authorization process has an invalid state. This should never happen.', 'unknown_state'); | ||
} | ||
/** | ||
* Computes `scope` URL parameter from scopes array. | ||
* | ||
* @param {Array<String>} scopes List of scopes to use with the request. | ||
* @return {String} Computed scope value. | ||
* Processes the response returned by the popup or the iframe. | ||
* @param {URLSearchParams} oauthParams | ||
* @returns {string[]} Parameters for the [reportOAuthError]() function | ||
*/ | ||
_computeScope(scopes) { | ||
if (!scopes) { | ||
return ''; | ||
} | ||
const scope = scopes.join(' '); | ||
return encodeURIComponent(scope); | ||
[createTokenResponseError](oauthParams) { | ||
const code = oauthParams.get('error'); | ||
const message = oauthParams.get('error_description'); | ||
return this[createErrorParams](code, message); | ||
} | ||
/** | ||
* Listens for a message from the popup. | ||
* @param {Event} e | ||
* Creates arguments for the error function from error response | ||
* @param {string} code Returned from the authorization server error code | ||
* @param {string=} description Returned from the authorization server error description | ||
* @returns {string[]} Parameters for the [reportOAuthError]() function | ||
*/ | ||
_popupMessageHandler(e) { | ||
if (!this._popup && !this._iframe) { | ||
return; | ||
[createErrorParams](code, description) { | ||
let message; | ||
if (description) { | ||
message = description; | ||
} else { | ||
switch (code) { | ||
case 'interaction_required': | ||
message = 'The request requires user interaction.'; | ||
break; | ||
case 'invalid_request': | ||
message = 'The request is missing a required parameter.'; | ||
break; | ||
case 'invalid_client': | ||
message = 'Client authentication failed.'; | ||
break; | ||
case 'invalid_grant': | ||
message = 'The provided authorization grant or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'; | ||
break; | ||
case 'unauthorized_client': | ||
message = 'The authenticated client is not authorized to use this authorization grant type.'; | ||
break; | ||
case 'unsupported_grant_type': | ||
message = 'The authorization grant type is not supported by the authorization server.'; | ||
break; | ||
case 'invalid_scope': | ||
message = 'The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.'; | ||
break; | ||
default: | ||
message = 'Unknown error'; | ||
} | ||
} | ||
this._processPopupData(e); | ||
return [message, code]; | ||
} | ||
_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(); | ||
} | ||
} | ||
/** | ||
* Creates a token info object from query parameters | ||
* @param {URLSearchParams} oauthParams | ||
* @return {TokenInfo} | ||
*/ | ||
[tokenInfoFromParams](oauthParams) { | ||
const accessToken = oauthParams.get('access_token'); | ||
const refreshToken = oauthParams.get('refresh_token'); | ||
const tokenType = oauthParams.get('token_type'); | ||
const expiresIn = Number(oauthParams.get('expires_in')); | ||
const scope = this[computeTokenInfoScopes](oauthParams.get('scope')); | ||
const tokenInfo = /** @type TokenInfo */ ({ | ||
accessToken, | ||
refreshToken, | ||
tokenType, | ||
expiresIn, | ||
state: oauthParams.get('state'), | ||
scope, | ||
expiresAt: undefined, | ||
expiresAssumed: false, | ||
}); | ||
return this[computeExpires](tokenInfo); | ||
} | ||
_clearIframeTimeout() { | ||
if (this.__frameLoadTimeout) { | ||
clearTimeout(this.__frameLoadTimeout); | ||
this.__frameLoadTimeout = undefined; | ||
/** | ||
* Processes token info object when it's ready. | ||
* | ||
* @param {TokenInfo} info Token info returned from the server. | ||
*/ | ||
[handleTokenInfo](info) { | ||
this[tokenResponse] = info; | ||
if (this[resolveFunction]) { | ||
this[resolveFunction](info); | ||
} | ||
this[rejectFunction] = undefined; | ||
this[resolveFunction] = 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. | ||
* Computes token expiration time. | ||
* It sets `expires_at` property on the token info object which is the time | ||
* in the future when when the token expires. | ||
* | ||
* @param {TokenInfo} tokenInfo Token info object | ||
* @returns {TokenInfo} A copy with updated properties. | ||
*/ | ||
_beforePopupUnloadHandler() { | ||
if (this.tokenInfo || (this._type === 'authorization_code' && this._exchangeCodeValue)) { | ||
return; | ||
[computeExpires](tokenInfo) { | ||
const copy = { ...tokenInfo }; | ||
let { expiresIn } = copy; | ||
if (!expiresIn || Number.isNaN(expiresIn)) { | ||
expiresIn = 3600; | ||
copy.expiresAssumed = true; | ||
} | ||
const settings = this._settings || {}; | ||
this._dispatchError({ | ||
message: 'No response has been recorded.', | ||
code: 'no_response', | ||
state: this._state, | ||
interactive: settings.interactive | ||
}); | ||
this.clear(); | ||
copy.expiresIn = expiresIn; | ||
const expiresAt = Date.now() + (expiresIn * 1000); | ||
copy.expiresAt = expiresAt; | ||
return copy; | ||
} | ||
/** | ||
* 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. | ||
* Computes the final list of granted scopes. | ||
* It is a list of scopes received in the response or the list of requested scopes. | ||
* Because the user may change the list of scopes during the authorization process | ||
* the received list of scopes can be different than the one requested by the user. | ||
* | ||
* @param {String} code Returned code from the authorization endpoint. | ||
* @return {Promise} Promise with token information. | ||
* @param {string} scope The `scope` parameter received with the response. It's null safe. | ||
* @return {string[]} The list of scopes for the token. | ||
*/ | ||
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); | ||
[computeTokenInfoScopes](scope) { | ||
const requestedScopes = this.settings.scopes; | ||
if (!scope && requestedScopes) { | ||
return requestedScopes; | ||
} | ||
let listScopes = []; | ||
if (scope) { | ||
listScopes = scope.split(' '); | ||
} | ||
return listScopes; | ||
} | ||
/** | ||
* Exchanges the authorization code for authorization token. | ||
* | ||
* @param {string} code Returned code from the authorization endpoint. | ||
* @returns {Promise<TokenInfo>} The token info when the request was a success. | ||
*/ | ||
async exchangeCode(code) { | ||
const body = this.getCodeRequestBody(code); | ||
const url = this.settings.accessTokenUri; | ||
return this.requestToken(url, body); | ||
} | ||
/** | ||
* 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. | ||
* @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); | ||
getCodeRequestBody(code) { | ||
const { settings } = this; | ||
const params = new URLSearchParams(); | ||
params.set('grant_type', 'authorization_code'); | ||
params.set('client_id', settings.clientId); | ||
if (settings.redirectUri) { | ||
url += '&redirect_uri=' + encodeURIComponent(settings.redirectUri); | ||
params.set('redirect_uri', settings.redirectUri); | ||
} | ||
url += '&code=' + encodeURIComponent(code); | ||
params.set('code', code); | ||
if (settings.clientSecret) { | ||
url += '&client_secret=' + encodeURIComponent(settings.clientSecret); | ||
params.set('client_secret', settings.clientSecret); | ||
} else { | ||
url += '&client_secret='; | ||
params.set('client_secret', ''); | ||
} | ||
return url; | ||
if (settings.pkce) { | ||
params.set('code_verifier', this[codeVerifierValue]); | ||
} | ||
return params.toString(); | ||
} | ||
/** | ||
* Requests for token from the authorization server for `code`, `password`, | ||
* `client_credentials` and custom grant types. | ||
* 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. | ||
* @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. | ||
* @return {Promise<TokenInfo>} Promise resolved to the response string. | ||
*/ | ||
_requestToken(url, body, settings) { | ||
async requestToken(url, body) { | ||
const urlInstance = new URL(url); | ||
const { settings } = this; | ||
let headers = { | ||
'content-type': 'application/x-www-form-urlencoded', | ||
}; | ||
if (settings.customData) { | ||
const cs = settings.customData.token; | ||
if (cs) { | ||
url = this._applyCustomSettingsQuery(url, cs); | ||
if (settings.customData.token) { | ||
applyCustomSettingsQuery(urlInstance, settings.customData.token); | ||
} | ||
body = this._applyCustomSettingsBody(body, settings.customData); | ||
body = applyCustomSettingsBody(body, settings.customData); | ||
headers = applyCustomSettingsHeaders(headers, settings.customData); | ||
} | ||
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)); | ||
} | ||
const init = /** @type RequestInit */ ({ | ||
headers, | ||
body, | ||
method: 'POST', | ||
cache: 'no-cache', | ||
}); | ||
} | ||
/** | ||
* 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; | ||
const response = await fetch(urlInstance.toString(), init); | ||
const { status } = response; | ||
let responseBody; | ||
try { | ||
tokenInfo = this._processCodeResponse(srvResponse, e.target.getResponseHeader('content-type')); | ||
responseBody = await response.text(); | ||
} catch (e) { | ||
reject(new Error(e.message)); | ||
return; | ||
responseBody = 'No response has been recorded'; | ||
} | ||
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; | ||
if (status === 404) { | ||
throw new Error('Authorization URI is invalid. Received status 404.'); | ||
} | ||
reject(new Error(message)); | ||
if (status >= 400 && status < 500) { | ||
throw new Error(`Client error: ${responseBody}`) | ||
} | ||
if (status >= 500) { | ||
throw new Error(`Authorization server error. Response code is: ${status}`) | ||
} | ||
return this[processCodeResponse](responseBody, response.headers.get('content-type')); | ||
} | ||
/** | ||
* Processes token request body and produces map of values. | ||
* Processes code response 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. | ||
* @param {string} body Body received in the response. | ||
* @param {string} mime Response content type. | ||
* @return {TokenInfo} Response as an object. | ||
* @throws {Error} Exception when the body is invalid. | ||
*/ | ||
_processCodeResponse(body, contentType) { | ||
[processCodeResponse](body, mime) { | ||
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]; | ||
let tokenInfo = {}; | ||
if (mime.includes('json')) { | ||
const info = JSON.parse(body); | ||
Object.keys(info).forEach((name) => { | ||
if (name.includes('_') || name.includes('-')) { | ||
name = camel(name); | ||
} | ||
tokenInfo[name] = info[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]); | ||
const params = new URLSearchParams(body); | ||
params.forEach((value, key) => { | ||
let name = key; | ||
if (key.includes('_') || key.includes('-')) { | ||
name = camel(key); | ||
} | ||
tokenInfo[name] = value; | ||
tokenInfo[camelName] = value; | ||
}); | ||
} | ||
return tokenInfo; | ||
if (tokenInfo.error) { | ||
throw new CodeError(tokenInfo.error, tokenInfo.errorDescription); | ||
} | ||
const expiresIn = Number(tokenInfo.expiresIn); | ||
const scope = this[computeTokenInfoScopes](tokenInfo.scope); | ||
const result = /** @type TokenInfo */ ({ | ||
...tokenInfo, | ||
expiresIn, | ||
scope, | ||
expiresAt: undefined, | ||
expiresAssumed: false, | ||
}); | ||
return this[computeExpires](result); | ||
} | ||
/** | ||
* 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. | ||
* A handler for the error that happened during code exchange. | ||
* @param {Error} e | ||
*/ | ||
_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 | ||
}); | ||
[handleTokenCodeError](e) { | ||
if (e instanceof CodeError) { | ||
this[reportOAuthError](...this[createErrorParams](e.message, e.code)); | ||
} else { | ||
this._dispatchResponse(tokenInfo); | ||
this[reportOAuthError](`Couldn't connect to the server. ${e.message}`, 'request_error'); | ||
} | ||
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. | ||
* Requests a token for `client_credentials` request type. | ||
* | ||
* This method resolves the main promise set by the `authorize()` function. | ||
* | ||
* @param {Object} settings The same settings as passed to `authorize()` | ||
* function. | ||
* @return {Promise} Promise resolved to token info. | ||
* @return {Promise<void>} Promise resolved to a token info object. | ||
*/ | ||
async authorizePassword(settings) { | ||
this._settings = settings; | ||
async [authorizeClientCredentials]() { | ||
const { settings } = this; | ||
const url = settings.accessTokenUri; | ||
const body = this._getPasswordBody(settings); | ||
const body = this.getClientCredentialsBody(); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
const tokenInfo = await this.requestToken(url, body); | ||
this[handleTokenInfo](tokenInfo); | ||
} catch (cause) { | ||
this._handleTokenCodeError(cause); | ||
this[handleTokenCodeError](cause); | ||
} | ||
} | ||
/** | ||
* Generates a payload message for password authorization. | ||
* 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. | ||
* @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); | ||
getClientCredentialsBody() { | ||
const { settings } = this; | ||
const params = new URLSearchParams(); | ||
params.set('grant_type', 'client_credentials'); | ||
if (settings.clientId) { | ||
url += '&client_id=' + encodeURIComponent(settings.clientId); | ||
params.set('client_id', settings.clientId); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
url += '&scope=' + encodeURIComponent(settings.scopes.join(' ')); | ||
if (settings.clientSecret) { | ||
params.set('client_secret', settings.clientSecret); | ||
} | ||
return url; | ||
if (Array.isArray(settings.scopes) && settings.scopes.length) { | ||
params.set('scope', settings.scopes.join(' ')); | ||
} | ||
return params.toString(); | ||
} | ||
/** | ||
* Requests a token for `client_credentials` request type. | ||
* | ||
* This method resolves the main promise set by the `authorize()` function. | ||
* | ||
* @param {Object} settings The same settings as passed to `authorize()` | ||
* function. | ||
* @return {Promise} Promise resolved to a token info object. | ||
* @return {Promise<void>} Promise resolved to a token info object. | ||
*/ | ||
async authorizeClientCredentials(settings) { | ||
this._settings = settings; | ||
async [authorizePassword]() { | ||
const { settings } = this; | ||
const url = settings.accessTokenUri; | ||
const body = this._getClientCredentialsBody(settings); | ||
const body = this.getPasswordBody(); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
const tokenInfo = await this.requestToken(url, body); | ||
this[handleTokenInfo](tokenInfo); | ||
} catch (cause) { | ||
this._handleTokenCodeError(cause); | ||
this[handleTokenCodeError](cause); | ||
} | ||
} | ||
/** | ||
* Generates a payload message for client credentials. | ||
* 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. | ||
* @return {string} Message body as defined in OAuth2 spec. | ||
*/ | ||
_getClientCredentialsBody(settings) { | ||
let url = 'grant_type=client_credentials'; | ||
getPasswordBody() { | ||
const { settings } = this; | ||
const params = new URLSearchParams(); | ||
params.set('grant_type', 'password'); | ||
params.set('username', settings.username); | ||
params.set('password', settings.password); | ||
if (settings.clientId) { | ||
url += '&client_id=' + encodeURIComponent(settings.clientId); | ||
params.set('client_id', settings.clientId); | ||
} | ||
if (settings.clientSecret) { | ||
url += '&client_secret=' + encodeURIComponent(settings.clientSecret); | ||
params.set('client_secret', settings.clientSecret); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
url += '&scope=' + this._computeScope(settings.scopes); | ||
if (Array.isArray(settings.scopes) && settings.scopes.length) { | ||
params.set('scope', settings.scopes.join(' ')); | ||
} | ||
return url; | ||
return params.toString(); | ||
} | ||
/** | ||
* Performs authorization on custom grant type. | ||
* This extension is described in OAuth 2.0 spec. | ||
* | ||
* This method resolves the main promise set by the `authorize()` function. | ||
* | ||
* @param {Object} settings Settings object as for `authorize()` function. | ||
* @return {Promise} Promise resolved to a token info object. | ||
* @return {Promise<void>} Promise resolved when the request finish. | ||
*/ | ||
async authorizeCustomGrant(settings) { | ||
this._settings = settings; | ||
async [authorizeCustomGrant]() { | ||
const { settings } = this; | ||
const url = settings.accessTokenUri; | ||
const body = this._getCustomGrantBody(settings); | ||
const body = this.getCustomGrantBody(); | ||
try { | ||
const tokenInfo = await this._requestToken(url, body, settings); | ||
const result = this._handleTokenInfo(tokenInfo); | ||
this.clear(); | ||
return result; | ||
const tokenInfo = await this.requestToken(url, body); | ||
this[handleTokenInfo](tokenInfo); | ||
} catch (cause) { | ||
this._handleTokenCodeError(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 | ||
* Generates a payload message for the custom grant. | ||
* | ||
* @param {Object} settings | ||
* @return {String} Request body. | ||
* @return {string} Message body as defined in OAuth2 spec. | ||
*/ | ||
_getCustomGrantBody(settings) { | ||
const parts = ['grant_type=' + encodeURIComponent(settings.type)]; | ||
getCustomGrantBody() { | ||
const { settings } = this; | ||
const params = new URLSearchParams(); | ||
params.set('grant_type', settings.grantType); | ||
if (settings.clientId) { | ||
parts[parts.length] = 'client_id=' + encodeURIComponent(settings.clientId); | ||
params.set('client_id', settings.clientId); | ||
} | ||
if (settings.clientSecret) { | ||
parts[parts.length] = 'client_secret=' + encodeURIComponent(settings.clientSecret); | ||
params.set('client_secret', settings.clientSecret); | ||
} | ||
if (settings.scopes && settings.scopes.length) { | ||
parts[parts.length] = 'scope=' + this._computeScope(settings.scopes); | ||
if (Array.isArray(settings.scopes) && settings.scopes.length) { | ||
params.set('scope', settings.scopes.join(' ')); | ||
} | ||
if (settings.redirectUri) { | ||
parts[parts.length] = 'redirect_uri=' + encodeURIComponent(settings.redirectUri); | ||
params.set('redirect_uri', settings.redirectUri); | ||
} | ||
if (settings.username) { | ||
parts[parts.length] = 'username=' + encodeURIComponent(settings.username); | ||
params.set('username', settings.username); | ||
} | ||
if (settings.password) { | ||
parts[parts.length] = 'password=' + encodeURIComponent(settings.password); | ||
params.set('password', settings.password); | ||
} | ||
return parts.join('&'); | ||
return params.toString(); | ||
} | ||
/** | ||
* 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) { | ||
// ... | ||
} | ||
}); | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
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
184096
24
3271
8
153
3
+ Added@advanced-rest-client/arc-events@0.2.34(transitive)
+ Added@advanced-rest-client/arc-types@0.2.62(transitive)
+ Addedcryptojslib@3.1.2(transitive)
+ Addedjsrsasign@10.9.0(transitive)
Updatedlit-element@^2.4.0