New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@advanced-rest-client/oauth-authorization

Package Overview
Dependencies
Maintainers
3
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@advanced-rest-client/oauth-authorization - npm Package Compare versions

Comparing version 4.0.3 to 5.0.0

src/AuthorizationError.js

38

CHANGELOG.md

@@ -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

18

index.d.ts

@@ -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);
{
"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": [

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc