From 0509e1f68026974222657f0869fa2a869ba4ca41 Mon Sep 17 00:00:00 2001 From: Aaron Granick Date: Mon, 2 Mar 2020 11:14:59 -0800 Subject: [PATCH] version 3.0.0 (#326) --- .eslintrc.json | 10 +- CHANGELOG.md | 38 + README.md | 378 +++--- THIRD-PARTY-NOTICES | 40 - package.json | 2 + packages/okta-auth-js/.eslintrc.json | 14 + packages/okta-auth-js/jest.server.js | 5 +- packages/okta-auth-js/jquery/index.js | 17 - packages/okta-auth-js/jquery/jqueryRequest.js | 37 - packages/okta-auth-js/lib/TokenManager.js | 22 +- packages/okta-auth-js/lib/browser/browser.js | 240 ++-- .../okta-auth-js/lib/browser/browserIndex.js | 2 +- .../lib/browser/browserStorage.js | 29 +- packages/okta-auth-js/lib/builderUtil.js | 36 +- packages/okta-auth-js/lib/crypto.js | 14 +- .../{ => lib}/fetch/fetchRequest.js | 58 +- packages/okta-auth-js/lib/http.js | 21 +- packages/okta-auth-js/lib/oauthUtil.js | 64 +- packages/okta-auth-js/lib/pkce.js | 15 +- packages/okta-auth-js/lib/server/server.js | 6 +- .../okta-auth-js/lib/server/serverIndex.js | 2 +- packages/okta-auth-js/lib/session.js | 10 +- packages/okta-auth-js/lib/storageBuilder.js | 2 +- packages/okta-auth-js/lib/token.js | 390 +++--- packages/okta-auth-js/lib/tx.js | 30 +- packages/okta-auth-js/lib/util.js | 13 +- packages/okta-auth-js/lib/vendor/polyfills.js | 86 -- packages/okta-auth-js/none/index.js | 15 - packages/okta-auth-js/package.json | 21 +- packages/okta-auth-js/reqwest/index.js | 17 - .../okta-auth-js/reqwest/reqwestRequest.js | 30 - packages/okta-auth-js/test/.eslintrc.json | 5 + .../okta-auth-js/test/karma/spec/crypto.js | 9 + .../okta-auth-js/test/karma/spec/loginFlow.js | 15 +- .../test/karma/spec/renewToken.js | 7 +- packages/okta-auth-js/test/spec/browser.js | 608 +++++---- .../okta-auth-js/test/spec/browserStorage.js | 192 +++ packages/okta-auth-js/test/spec/cookies.js | 34 +- packages/okta-auth-js/test/spec/errors.js | 18 +- packages/okta-auth-js/test/spec/features.js | 10 +- .../okta-auth-js/test/spec/fetch-request.js | 199 ++- .../okta-auth-js/test/spec/fingerprint.js | 8 +- packages/okta-auth-js/test/spec/general.js | 2 +- .../okta-auth-js/test/spec/mfa-challenge.js | 19 +- .../test/spec/mfa-enroll-activate.js | 16 +- .../okta-auth-js/test/spec/mfa-required.js | 8 +- packages/okta-auth-js/test/spec/oauthUtil.js | 97 +- packages/okta-auth-js/test/spec/pkce.js | 15 +- packages/okta-auth-js/test/spec/session.js | 51 +- .../okta-auth-js/test/spec/storageBuilder.js | 134 ++ packages/okta-auth-js/test/spec/token.js | 1150 +++++++++++------ .../okta-auth-js/test/spec/tokenManager.js | 91 +- packages/okta-auth-js/test/spec/util.js | 1 + .../okta-auth-js/webpack.common.config.js | 12 + packages/okta-auth-js/webpack.config.js | 3 +- test/app/env.js | 15 + test/app/package.json | 11 +- test/app/public/index.html | 1 + test/app/public/server/index.html | 40 + test/app/server.js | 67 + test/app/src/config.js | 23 +- test/app/src/form.js | 28 +- test/app/src/testApp.js | 79 +- test/app/src/tokens.js | 24 +- test/app/src/util.js | 5 +- test/app/src/webpackEntry.js | 2 +- test/app/webpack.config.js | 19 +- test/e2e/pageobjects/TestApp.js | 21 +- test/e2e/pageobjects/TestServer.js | 53 + test/e2e/specs/login.js | 16 +- test/e2e/specs/logout.js | 82 +- test/e2e/specs/server.js | 31 + test/e2e/specs/tokens.js | 7 +- test/e2e/util/appUtils.js | 2 +- test/e2e/util/loginUtils.js | 2 +- test/support/factory.js | 1 + test/support/oauthUtil.js | 59 +- test/support/tokens.js | 6 + test/support/util.js | 85 +- yarn.lock | 380 +++--- 80 files changed, 3293 insertions(+), 2134 deletions(-) create mode 100644 packages/okta-auth-js/.eslintrc.json delete mode 100644 packages/okta-auth-js/jquery/index.js delete mode 100644 packages/okta-auth-js/jquery/jqueryRequest.js rename packages/okta-auth-js/{ => lib}/fetch/fetchRequest.js (58%) delete mode 100644 packages/okta-auth-js/lib/vendor/polyfills.js delete mode 100644 packages/okta-auth-js/none/index.js delete mode 100644 packages/okta-auth-js/reqwest/index.js delete mode 100644 packages/okta-auth-js/reqwest/reqwestRequest.js create mode 100644 packages/okta-auth-js/test/.eslintrc.json create mode 100644 packages/okta-auth-js/test/spec/browserStorage.js create mode 100644 packages/okta-auth-js/test/spec/storageBuilder.js create mode 100644 test/app/env.js create mode 100644 test/app/public/server/index.html create mode 100644 test/app/server.js create mode 100644 test/e2e/pageobjects/TestServer.js create mode 100644 test/e2e/specs/server.js diff --git a/.eslintrc.json b/.eslintrc.json index d21257df4..18869c248 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,9 +1,17 @@ { "extends": ["eslint:recommended"], "env": { - "browser": true, + "browser": false, "commonjs": true }, + "globals": { + "Promise": true, + "console": true, + "setTimeout": true, + "clearTimeout": true, + "setInterval": true, + "clearInterval": true + }, "rules": { "camelcase": 2, "complexity": [2, 7], diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9c78c47..3b8757b96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## 3.0.0 + +### Breaking Changes + +- [#308](https://github.com/okta/okta-auth-js/pull/308) - Removed `jquery` and `reqwest` httpRequesters + +- [#309](https://github.com/okta/okta-auth-js/pull/309) - Removed `Q` library, now using standard Promise. IE11 will require a polyfill for the `Promise` object. Use of `Promise.prototype.finally` requires Node > 10.3 for server-side use. + +- [#310](https://github.com/okta/okta-auth-js/pull/310) - New behavior for [signOut()](README.md#signout) + - `postLogoutRedirectUri` will default to `window.location.origin` + - [signOut()](README.md#signout) will revoke access token and perform redirect by default. Fallback to XHR [closeSession()](README.md#closesession) if no idToken. + - New method [closeSession()](README.md#closesession) for XHR signout without redirect or reload. + - New method [revokeAccessToken()](README.md#revokeaccesstokenaccesstoken) + +- [#311](https://github.com/okta/okta-auth-js/pull/311) - [parseFromUrl()](README.md#tokenparsefromurloptions) now returns tokens in an object hash (instead of array). The `state` parameter (passed to authorize request) is also returned. + +- [#313](https://github.com/okta/okta-auth-js/pull/313) - New [option](README.md#additional-options) `secureCookies`, which is `true` by default. An HTTPS origin will be enforced unless `secureCookies` is set to `false`. + +- [#316](https://github.com/okta/okta-auth-js/pull/316) - Option `issuer` is [required](README.md#configuration-reference). Option `url` has been deprecated and is no longer used. + +- [#317](https://github.com/okta/okta-auth-js/pull/317) - `pkce` [option](README.md#additional-options) is now `true` by default. `grantType` option is removed. + +- [#320](https://github.com/okta/okta-auth-js/pull/320) - `getWithRedirect`, `getWithPopup`, and `getWithoutPrompt` previously took 2 sets of option objects as parameters, a set of "oauthOptions" and additional options. These methods now take a single options object which can hold all [available options](README.md#authorize-options). Passing a second options object will cause an exception to be thrown. + +- [#321](https://github.com/okta/okta-auth-js/pull/321) + - Default responseType when using [implicit flow](README.md#implicit-oauth-20-flow) is now `['token', 'id_token']`. + - When both access token and id token are returned, the id token's `at_hash` claim will be validated against the access token + +- [#325](https://github.com/okta/okta-auth-js/pull/325) - Previously, the default `responseMode` for [PKCE](README.md#pkce-oauth-20-flow) was `"fragment"`. It is now `"query"`. Unless explicitly specified using the `responseMode` option, the `response_mode` parameter is no longer passed by `token.getWithRedirect` to the `/authorize` endpoint. The `response_mode` will be set by the backend according to the [OpenID specification](https://openid.net/specs/openid-connect-core-1_0.html#Authentication). [Implicit flow](README.md#implicit-oauth-20-flow) will use `"fragment"` and [PKCE](README.md#pkce-oauth-20-flow) will use `"query"`. If previous behavior is desired, [PKCE](README.md#pkce-oauth-20-flow) can set the `responseMode` option to `"fragment"`. + +- [#329](https://github.com/okta/okta-auth-js/pull/329) - Fix internal fetch implementation. `responseText` will always be a string, regardless of headers or response type. If a JSON object was returned, the object will be returned as `responseJSON` and `responseType` will be set to "json". Invalid/malformed JSON server response will no longer throw a raw TypeError but will return a well structured error response which includes the `status` code returned from the server. + +### Other + +- [#306](https://github.com/okta/okta-auth-js/pull/306) - Now using babel for ES5 compatibility. [All polyfills have been removed](README.md#browser-compatibility). + +- [#312](https://github.com/okta/okta-auth-js/pull/312) - Added an E2E test for server-side authentication (node module, not webpack). + ## 2.13.1 ### Bug Fixes diff --git a/README.md b/README.md index 574576d29..551d7cb57 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,10 @@ * [Node JS Usage](#node-js-usage) * [Contributing](#contributing) -The Okta Auth JavaScript SDK builds on top of our [Authentication API](https://developer.okta.com/docs/api/resources/authn) and [OAuth 2.0 API](https://developer.okta.com/docs/api/resources/oidc) to enable you to create a fully branded sign-in experience using JavaScript. +The Okta Auth JavaScript SDK builds on top of our [Authentication API](https://developer.okta.com/docs/api/resources/authn) and [OpenID Connect & OAuth 2.0 API](https://developer.okta.com/docs/api/resources/oidc) to enable you to create a fully branded sign-in experience using JavaScript. You can learn more on the [Okta + JavaScript][lang-landing] page in our documentation. -## Release status - This library uses semantic versioning and follows Okta's [library version policy](https://developer.okta.com/code/library-versions/). :heavy_check_mark: The current stable major version series is: `2.x` @@ -41,6 +39,16 @@ If you run into problems using the SDK, you can: * Ask questions on the [Okta Developer Forums][devforum] * Post [issues][github-issues] here on GitHub (for code errors) +### Browser Compatibility + +This SDK is known to work with current versions of Chrome, Firefox, and Safari on desktop and mobile. + +Compatibility with IE Edge can be accomplished by adding polyfill/shims for the following objects: + +* Promise +* Array.from +* TextEncoder + ## Getting started Installing the Authentication SDK is simple. You can include it in your project via our npm package, [@okta/okta-auth-js](https://www.npmjs.com/package/@okta/okta-auth-js). @@ -48,6 +56,29 @@ Installing the Authentication SDK is simple. You can include it in your project You'll also need: * An Okta account, called an _organization_ (sign up for a free [developer organization](https://developer.okta.com/signup) if you need one) +* An Okta application, which can be created using the Okta Admin UI + +### Creating your Okta application + +When creating a new Okta application, you can specify the application type. This SDK is designed to work with `SPA` (Single-page Applications) or `Web` applications. A `SPA` application will perform all logic and authorization flows client-side. A `Web` application will perform authorization flows on the server. + +### Configuring your Okta application + +From the Okta Admin UI, click `Applications`, then select your application. You can view and edit your Okta application's configuration under the application's `General` tab. + +#### Client ID + +A string which uniquely identifies your Okta application. + +#### Login redirect URIs + +To sign users in, your application redirects the browser to an Okta-hosted sign-in page. Okta then redirects back to your application with information about the user. You can learn more about how this works on [Okta-hosted flows](https://developer.okta.com/docs/concepts/okta-hosted-flows/). + +You need to whitelist the login redirect URL in your Okta application settings. + +#### Logout redirect URIs + +After you sign users out of your app and out of Okta, you have to redirect users to a specific location in your application. You need to whitelist the post sign-out URL in your Okta application settings. ### Using the npm module @@ -76,16 +107,6 @@ var OktaAuth = require('@okta/okta-auth-js'); var authClient = new OktaAuth(/* configOptions */); ``` -If you're using a bundler like webpack or browserify, we have implementations for jquery and reqwest included. To use them, import the SDK like this: - -```javascript -// reqwest -var OktaAuth = require('@okta/okta-auth-js/reqwest'); - -// jquery -var OktaAuth = require('@okta/okta-auth-js/jquery'); -``` - ## Usage guide For an overview of the client's features and authentication flows, check out [our developer docs](https://developer.okta.com/code/javascript/okta_auth_sdk). There, you will learn how to use the Auth SDK on a simple static page to: @@ -97,7 +118,21 @@ You can also browse the full [API reference documentation](#api-reference). ## Configuration reference -If you are using this SDK to implement an OIDC flow, the only required configuration option is `issuer`: +Whether you are using this SDK to implement an OIDC flow or for communicating with the [Authentication API](https://developer.okta.com/docs/api/resources/authn), the only required configuration option is `issuer`, which is the URL to an Okta [Authorization Server](https://developer.okta.com/docs/guides/customize-authz-server/overview/) + +### About the Issuer + +You may use the URL for your Okta organization as the issuer. This will apply a default authorization policy and issue tokens scoped at the organization level. + +```javascript +var config = { + issuer: 'https://{yourOktaDomain}' +}; + +var authClient = new OktaAuth(config); +``` + +Okta allows you to create multiple custom OAuth 2.0 authorization servers that you can use to protect your own resource servers. Within each authorization server you can define your own OAuth 2.0 scopes, claims, and access policies. Many organizations have a "default" authorization server. ```javascript var config = { @@ -107,18 +142,18 @@ var config = { var authClient = new OktaAuth(config); ``` -If you’re using this SDK only for communicating with the [Authentication API](https://developer.okta.com/docs/api/resources/authn), you instead need to set the `url` for your Okta Domain: +You may also create and customize additional authorization servers. ```javascript + var config = { - // The URL for your Okta organization - url: 'https://{yourOktaDomain}' + issuer: 'https://{yourOktaDomain}/oauth2/custom-auth-server-id' }; var authClient = new OktaAuth(config); ``` -### [OpenID Connect](https://developer.okta.com/docs/api/resources/oidc) options +### Configuration options These configuration options can be included when instantiating Okta Auth JS (`new OktaAuth(config)`) or in `token.getWithoutPrompt`, `token.getWithPopup`, or `token.getWithRedirect` (unless noted otherwise). If included in both, the value passed in the method takes priority. @@ -188,30 +223,44 @@ tokenManager: { } ``` +#### responseMode + +When requesting tokens using [token.getWithRedirect](#tokengetwithredirectoptions) values will be returned as parameters appended to the [redirectUri](#additional-options). + +In most cases you will not need to set a value for `responseMode`. Defaults are set according to the [OpenID Connect 1.0 specification](https://openid.net/specs/openid-connect-core-1_0.html#Authentication). + +* For [PKCE OAuth Flow](#pkce-oauth-20-flow)), the authorization code will be in search query of the URL. Clients using the PKCE flow can opt to instead receive the authorization code in the hash fragment by setting the [responseMode](#additional-options) option to "fragment". + +* For [Implicit OAuth Flow](#implicit-oauth-20-flow)), tokens will be in the hash fragment of the URL. This cannot be changed. + +#### Required Options + +| Option | Description | +| -------------- | ------------ | +| `issuer` | The URL for your Okta organization or an Okta authentication server. [About the issuer](#about-the-issuer) | + #### Additional Options | Option | Description | | -------------- | ------------ | -| `issuer` | Specify a custom issuer to perform the OIDC flow. Defaults to the base url parameter if not provided. | -| `clientId` | Client Id pre-registered with Okta for the OIDC authentication flow. | -| `redirectUri` | The url that is redirected to when using `token.getWithRedirect`. This must be pre-registered as part of client registration. If no `redirectUri` is provided, defaults to the current origin. | -| `postLogoutRedirectUri` | Specify the url where the browser should be redirected after [signOut](#signout). This url must be added to the list of `Logout redirect URIs` on the application's `General Settings` tab. -| `pkce` | If set to true, the authorization flow will automatically use PKCE. The authorize request will use `response_type=code`, and `grant_type=authorization_code` will be used on the token request. All these details are handled for you, including the creation and verification of code verifiers. | -| `responseMode` | Applicable only for SPA clients using PKCE flow. By default, when requesting tokens via redirect (Initiated with `token.getWithRedirect` and handled using `token.parseFromUrl`), the PKCE authorization code is requested and parsed from the hash fragment. Setting this value to `query` will cause the URL search query to be used instead. If your application uses or alters the hash fragment of the url, you may want to set this option to "query". | +| `clientId` | Client Id pre-registered with Okta for the OIDC authentication flow. [Creating your Okta application](#creating-your-okta-appliation) | +| `redirectUri` | The url that is redirected to when using `token.getWithRedirect`. This must be listed in your Okta application's [Login redirect URIs](#login-redirect-uris). If no `redirectUri` is provided, defaults to the current origin (`window.location.origin`). [Configuring your Okta application](#configuring-your-okta-application) | +| `postLogoutRedirectUri` | Specify the url where the browser should be redirected after [signOut](#signout). This url must be listed in your Okta application's [Logout redirect URIs](#logout-redirect-uris). If not specified, your application's origin (`window.location.origin`) will be used. [Configuring your Okta application](#configuring-your-okta-application) | +| `onSessionExpired` | A function to be called when the Okta SSO session has expired or was ended outside of the application. A typical handler would initiate a login flow. | +| `responseMode` | Applicable only for SPA clients using [PKCE OAuth Flow](#pkce-oauth-20-flow). By default, the authorization code is requested and parsed from the search query. Setting this value to `fragment` will cause the URL hash fragment to be used instead. If your application uses or alters the search query portion of the `redirectUri`, you may want to set this option to "fragment". This option affects both [token.getWithRedirect](#tokengetwithredirectoptions) and [token.parseFromUrl](#tokenparsefromurloptions) | +| `secureCookies` | Defaults to `true`. Will set the "secure" option on all cookies. With this option on, an exception will be thrown if the application origin is not using the HTTPS protocol. Automatically disabled for `http://localhost`. | +| `pkce` | Enable the [PKCE OAuth Flow](#pkce-oauth-20-flow). Default value is `true`. If set to `false`, the authorization flow will use the [Implicit OAuth Flow](#implicit-oauth-20-flow). When PKCE flow is enabled the authorize request will use `response_type=code` and `grant_type=authorization_code` on the token request. All these details are handled for you, including the creation and verification of code verifiers. Tokens can be retrieved on the login callback by calling [token.parseFromUrl](#tokenparsefromurloptions) | | `authorizeUrl` | Specify a custom authorizeUrl to perform the OIDC flow. Defaults to the issuer plus "/v1/authorize". | | `userinfoUrl` | Specify a custom userinfoUrl. Defaults to the issuer plus "/v1/userinfo". | | `tokenUrl` | Specify a custom tokenUrl. Defaults to the issuer plus "/v1/token". | | `ignoreSignature` | ID token signatures are validated by default when `token.getWithoutPrompt`, `token.getWithPopup`, `token.getWithRedirect`, and `token.verify` are called. To disable ID token signature validation for these methods, set this value to `true`. | | | This option should be used only for browser support and testing purposes. | -| `maxClockSkew` | Defaults to 300 (five minutes). This is the maximum difference allowed between a client's clock and Okta's, in seconds, when validating tokens. Setting this to 0 is not recommended, because it increases the likelihood that valid tokens will fail validation. -| `onSessionExpired` | A function to be called when the Okta SSO session has expired or was ended outside of the application. A typical handler would initiate a login flow. -| `tokenManager` | *(optional)*: An object containing additional properties used to configure the internal token manager. | +| `maxClockSkew` | Defaults to 300 (five minutes). This is the maximum difference allowed between a client's clock and Okta's, in seconds, when validating tokens. Setting this to 0 is not recommended, because it increases the likelihood that valid tokens will fail validation. | +| `tokenManager` | An object containing additional properties used to configure the internal token manager. | * `autoRenew`: By default, the library will attempt to renew expired tokens. When an expired token is requested by the library, a renewal request is executed to update the token. If you wish to to disable auto renewal of tokens, set autoRenew to false. -* `secure`: If `true` then only "secure" https cookies will be stored. This option will prevent cookies from being stored on an HTTP connection. This option is only relevant if `storage` is set to `cookie`, or if the client browser does not support `localStorage` or `sessionStorage`, in which case `cookie` storage will be used. - * `storage`: You may pass an object or a string. If passing an object, it should meet the requirements of a [custom storage provider](#storage). Pass a string to specify one of the built-in storage types: * [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) (default) @@ -225,24 +274,25 @@ tokenManager: { ```javascript var config = { - url: 'https://{yourOktaDomain}', - - // Optional config + // Required config issuer: 'https://{yourOktaDomain}/oauth2/default', + + // Required for login flow using getWithRedirect() clientId: 'GHtf9iJdr60A9IYrR0jw', redirectUri: 'https://acme.com/oauth2/callback/home', - // Override the default authorize and userinfo URLs - authorizeUrl: 'https://{yourOktaDomain}/oauth2/default/v1/authorize', - userinfoUrl: 'https://{yourOktaDomain}/oauth2/default/v1/userinfo', + // Parse authorization code from hash fragment instead of search query + responseMode: 'fragment', - // TokenManager config + // Configure TokenManager to use sessionStorage instead of localStorage tokenManager: { storage: 'sessionStorage' }, + // Handle session expiration / token renew failure onSessionExpired: function() { console.log('re-authorization is required'); + authClient.getWithRedirect(); } }; @@ -251,14 +301,22 @@ var authClient = new OktaAuth(config); ##### PKCE OAuth 2.0 flow -By default the `implicit` OAuth flow will be used. It is widely supported by most browsers. PKCE is a newer flow which is more secure, but does require certain capabilities from the browser. Specifically, the browser must implement `crypto.subtle` (also known as `webcrypto`). [Most modern browsers provide this](https://caniuse.com/#feat=cryptography) when running in a secure context (on an HTTPS connection). PKCE also requires the [TextEncoder](https://caniuse.com/#feat=textencoder) object. This is available on all major platforms except IE Edge. In this case, we recommend using a polyfill/shim such as [text-encoding](https://www.npmjs.com/package/text-encoding). +The PKCE OAuth flow will be used by default. It is widely supported by most modern browsers when running on an HTTPS connection. PKCE requires that the browser implements `crypto.subtle` (also known as `webcrypto`). [Most modern browsers provide this](https://caniuse.com/#feat=cryptography) when running in a secure context (on an HTTPS connection). PKCE also requires the [TextEncoder](https://caniuse.com/#feat=textencoder) object. This is available on all major browsers except IE Edge. In this case, we recommend using a polyfill/shim such as [text-encoding](https://www.npmjs.com/package/text-encoding). -To use PKCE flow, set `pkce` to `true` in your config. +If the user's browser does not support PKCE, an exception will be thrown. You can test if a browser supports PKCE before construction with this static method: + +`OktaAuth.features.isPKCESupported()` + +#### Implicit OAuth 2.0 flow + +Implicit OAuth flow is available as an option if PKCE flow cannot be supported in your deployment. It is widely supported by most browsers, and can work over an insecure HTTP connection. Note that implicit flow is less secure than PKCE flow, even over HTTPS, since raw tokens are exposed in the browser's history. For this reason, we highly recommending using the PKCE flow if possible. + +Implicit flow can be enabled by setting the `pkce` option to `false` ```javascript var config = { - pkce: true, + pkce: false, // other config issuer: 'https://{yourOktaDomain}/oauth2/default', @@ -267,15 +325,11 @@ var config = { var authClient = new OktaAuth(config); ``` -If the user's browser does not support PKCE, an exception will be thrown. You can test if a browser supports PKCE before construction with this static method: - -`OktaAuth.features.isPKCESupported()` - ### Optional configuration options ### `httpRequestClient` -The http request implementation. By default, this is implemented using [reqwest](https://github.com/ded/reqwest) for browser and [cross-fetch](https://github.com/lquixada/cross-fetch) for server. To provide your own request library, implement the following interface: +The http request implementation. By default, this is implemented using [cross-fetch](https://github.com/lquixada/cross-fetch). To provide your own request library, implement the following interface: 1. Must accept: * method (http method) @@ -300,38 +354,12 @@ var config = { } ``` -#### `ajaxRequest` - -:warning: This parameter has been *deprecated*, please use [**httpRequestClient**](#httpRequestClient) instead. - -The ajax request implementation. By default, this is implemented using [reqwest](https://github.com/ded/reqwest). To provide your own request library, implement the following interface: - - 1. Must accept: - * method (http method) - * url (target url) - * args (object containing headers and data) - 2. Must return a Promise that resolves with a raw XMLHttpRequest response - -```javascript -var config = { - url: 'https://{yourOktaDomain}', - ajaxRequest: function(method, url, args) { - // args is in the form: - // { - // headers: { - // headerName: headerValue - // }, - // data: postBodyData - // } - return Promise.resolve(/* a raw XMLHttpRequest response */); - } -} -``` - ## API Reference * [signIn](#signinoptions) * [signOut](#signout) +* [closeSession](#closesession) +* [revokeAccessToken](#revokeaccesstokenaccesstoken) * [forgotPassword](#forgotpasswordoptions) * [unlockAccount](#unlockaccountoptions) * [verifyRecoveryToken](#verifyrecoverytokenoptions) @@ -357,13 +385,13 @@ var config = { * [session.get](#sessionget) * [session.refresh](#sessionrefresh) * [token](#token) - * [token.getWithoutPrompt](#tokengetwithoutpromptoauthoptions) - * [token.getWithPopup](#tokengetwithpopupoauthoptions) + * [token.getWithoutPrompt](#tokengetwithoutpromptoptions) + * [token.getWithPopup](#tokengetwithpopupoptions) * [token.getWithRedirect](#tokengetwithredirectoptions) * [token.parseFromUrl](#tokenparsefromurloptions) * [token.decode](#tokendecodeidtokenstring) * [token.renew](#tokenrenewtokentorenew) - * [token.getUserInfo](#tokengetuserinfoaccesstokenobject) + * [token.getUserInfo](#tokengetuserinfoaccesstokenobject-idtokenobject) * [token.verify](#tokenverifyidtokenobject) * [tokenManager](#tokenmanager) * [tokenManager.add](#tokenmanageraddkey-token) @@ -396,58 +424,37 @@ authClient.signIn({ throw 'We cannot handle the ' + transaction.status + ' status'; } }) -.fail(function(err) { +.catch(function(err) { console.error(err); }); ``` ### `signOut()` -Signs the user out of their current [Okta session](https://developer.okta.com/docs/api/resources/sessions) and clears all tokens stored locally in the `TokenManager`. The Okta session can be closed using either an `XHR` method or a `redirect` method. By default, the `XHR` method is used. Here are some points to consider when deciding which method is most appropriate for your application: +Signs the user out of their current [Okta session](https://developer.okta.com/docs/api/resources/sessions) and clears all tokens stored locally in the `TokenManager`. By default, the access token is revoked so it can no longer be used. Some points to consider: -XHR: - -* Executes in the background. The user will see not any change to `window.location`. -* Will fail if 3rd-party cookies are blocked by the browser. -* If the session is already closed, the method will fail and reject the returned Promise. This error can be caught by the app and handled seamlessly. -* It is recommended (but not required) for the app to call `window.location.reload()` after the `XHR` method completes to ensure your app is properly re-initialized in an unauthenticated state. -* For more information, see [Close Current Session](https://developer.okta.com/docs/reference/api/sessions/#close-current-session) in the Session API documentation. - -Redirect: - -* Will change the `window.location` to an Okta-hosted page before redirecting to a URI of your choice. -* No issue with 3rd-party cookies. -* Requires a `postLogoutRedirectUri` to be specified. This URI must be whitelisted in the Okta application's settings. If the `postLogoutRedirectUri` is unknown or invalid the redirect will end on a 400 error page from Okta. This error will be visible to the user and cannot be handled by the app. -* Requires a valid ID token. If an ID token is not available, `signOut` will fallback to using the `XHR` method and then redirect to the `postLogoutRedirectUri`. +* Will redirect to an Okta-hosted page before returning to your app. +* If a `postLogoutRedirectUri` has not been specified or configured, `window.location.origin` will be used as the return URI. This URI must be listed in the Okta application's [Login redirect URIs](#login-redirect-uris). If the URI is unknown or invalid the redirect will end on a 400 error page from Okta. This error will be visible to the user and cannot be handled by the app. +* Requires a valid ID token. If an ID token is not available, `signOut` will fallback to using the XHR-based [closeSession](#closesession) method. This method may fail to sign the user out if 3rd-party cookies have been blocked by the browser. * For more information, see [Logout](https://developer.okta.com/docs/reference/api/oidc/#logout) in the OIDC API documentation. `signOut` takes the following options: -* `postLogoutRedirectUri` - Setting a value will enable the logout redirect method. **The URI must be whitelisted** as a `Logout Redirect Uri` in your application's general settings. It is common to use your application's origin URI as the `postLogoutRedirectUri` so that users are sent to the application "home page" after signout. +* `postLogoutRedirectUri` - Setting a value will override the `postLogoutRedirectUri` configured on the SDK. * `state` - An optional value, used along with `postLogoutRedirectUri`. If set, this value will be returned as a query parameter during the redirect to the `postLogoutRedirectUri` -* `idToken` - Specifies the ID token object. This is only relevant when a `postLogoutRedirectUri` has been specified. By default, `signOut` will look for a token object named `idToken` within the `TokenManager`. If you have stored the id token object in a different location, you should retrieve it first and then pass it here. -* `revokeAccessToken` - If `true`, the access token will be revoked before the session is closed. -* `accessToken` - Specifies the access token object. This is only relevant when the `revokeAccessToken` option is `true`. By default, `signOut` will look for a token object named `token` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. +* `idToken` - Specifies the ID token object. By default, `signOut` will look for a token object named `idToken` within the `TokenManager`. If you have stored the id token object in a different location, you should retrieve it first and then pass it here. +* `revokeAccessToken` - If `false`, the access token will not be revoked. Use this option with care: not revoking the access token may pose a security risk if the token has been leaked outside the application. +* `accessToken` - Specifies the access token object. By default, `signOut` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. This options is ignored if the `revokeAccessToken` option is `false`. ```javascript -// Sign out using the XHR method +// Sign out using the default options authClient.signOut() -.then(function() { - console.log('successfully logged out'); -}) -.catch(function(err) { - console.error(err); -}) -.then(function() { - // Reload app in unauthenticated state - window.location.reload(); -}); ``` ```javascript -// Sign out using the redirect method, specifying the current window origin as the post logout URI +// Override the post logout URI for this call authClient.signOut({ - postLogoutRedirectUri: window.location.origin + postLogoutRedirectUri: `${window.location.origin}/logout/callback` }); ``` @@ -455,29 +462,45 @@ authClient.signOut({ // In this case, the ID token is stored under the 'myIdToken' key var idToken = await authClient.tokenManager.get('myIdToken'); authClient.signOut({ - idToken: idToken, - postLogoutRedirectUri: window.location.origin + idToken: idToken }); ``` ```javascript -// Revoke the access token and sign out using the redirect method +// In this case, the access token is stored under the 'myAccessToken' key +var accessToken = await authClient.tokenManager.get('myAccessToken'); authClient.signOut({ - revokeAccessToken: true, - postLogoutRedirectUri: window.location.origin + accessToken: accessToken }); ``` +### `closeSession()` + +Signs the user out of their current [Okta session](https://developer.okta.com/docs/api/resources/sessions) and clears all tokens stored locally in the `TokenManager`. This method is an XHR-based alternative to [signOut](#signout), which will redirect to Okta before returning to your application. Here are some points to consider when using this method: + +* Executes in the background. The user will see not any change to `window.location`. +* Will fail to sign the user out if 3rd-party cookies are blocked by the browser. +* Does not revoke the access token. It is strongly recommended to call [revokeAccessToken](#revokeaccesstokenaccesstoken) before calling this method +* It is recommended (but not required) for the app to call `window.location.reload()` after the `XHR` method completes to ensure your app is properly re-initialized in an unauthenticated state. +* For more information, see [Close Current Session](https://developer.okta.com/docs/reference/api/sessions/#close-current-session) in the Session API documentation. + ```javascript -// In this case, the access token is stored under the 'myAccessToken' key -var accessToken = await authClient.tokenManager.get('myAccessToken'); -authClient.signOut({ - accessToken: accessToken, - revokeAccessToken: true, - postLogoutRedirectUri: window.location.origin -}); +await authClient.revokeAccessToken(); // strongly recommended +authClient.closeSession() + .then(() => { + window.location.reload(); // optional + }) + .catch(e => { + if (e.xhr && e.xhr.status === 429) { + // Too many requests + } + }) ``` +### `revokeAccessToken(accessToken)` + +Revokes the access token for this application so it can no longer be used to authenticate API requests. The `accessToken` parameter is optional. By default, `revokeAccessToken` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. Returns a promise that resolves when the operation has completed. This method will succeed even if the access token has already been revoked or removed. + ### `forgotPassword(options)` Starts a [new password recovery transaction](https://developer.okta.com/docs/api/resources/authn#forgot-password) for a given user and issues a recovery token that can be used to reset a user’s password. @@ -503,7 +526,7 @@ authClient.forgotPassword({ throw 'We cannot handle the ' + transaction.status + ' status'; } }) -.fail(function(err) { +.catch(function(err) { console.error(err); }); ``` @@ -533,7 +556,7 @@ authClient.unlockAccount({ throw 'We cannot handle the ' + transaction.status + ' status'; } }) -.fail(function(err) { +.catch(function(err) { console.error(err); }); ``` @@ -555,7 +578,7 @@ authClient.verifyRecoveryToken({ throw 'We cannot handle the ' + transaction.status + ' status'; } }) -.fail(function(err) { +.catch(function(err) { console.error(err); }); ``` @@ -575,7 +598,7 @@ authClient.webfinger({ .then(function(res) { // use the webfinger response to select an idp }) -.fail(function(err) { +.catch(function(err) { console.error(err); }); ``` @@ -591,7 +614,7 @@ authClient.fingerprint() .then(function(fingerprint) { // Do something with the fingerprint }) -.fail(function(err) { +.catch(function(err) { console.log(err); }) ``` @@ -607,7 +630,7 @@ if (exists) { .then(function(transaction) { console.log('current status:', transaction.status); }) - .fail(function(err) { + .catch(function(err) { console.error(err); }); } @@ -1503,21 +1526,26 @@ authClient.session.refresh() ### `token` -#### Extended OpenID Connect options +#### Authorize options The following configuration options can **only** be included in `token.getWithoutPrompt`, `token.getWithPopup`, or `token.getWithRedirect`. | Options | Description | | :-------: | ----------| | `sessionToken` | Specify an Okta sessionToken to skip reauthentication when the user already authenticated using the Authentication Flow. | -| `responseMode` | Specify how the authorization response should be returned. You will generally not need to set this unless you want to override the default values for `token.getWithRedirect`. See [Parameter Details](https://developer.okta.com/docs/api/resources/oidc#parameter-details) for a list of available modes. | -| `responseType` | Specify the [response type](https://developer.okta.com/docs/api/resources/oidc#request-parameters) for OIDC authentication. The default value is `id_token`. If `pkce` is `true`, this option will be ingored. | -| | Use an array if specifying multiple response types - in this case, the response will contain both an ID Token and an Access Token. `responseType: ['id_token', 'token']` | +| `responseType` | Specify the [response type](https://developer.okta.com/docs/api/resources/oidc#request-parameters) for OIDC authentication when using the [Implicit OAuth Flow](#implicit-oauth-20-flow). The default value is `['token', 'id_token']` which will request both an access token and ID token. If `pkce` is `true`, both the access and ID token will be requested and this option will be ignored. | + | `scopes` | Specify what information to make available in the returned `id_token` or `access_token`. For OIDC, you must include `openid` as one of the scopes. Defaults to `['openid', 'email']`. For a list of available scopes, see [Scopes and Claims](https://developer.okta.com/docs/api/resources/oidc#access-token-scopes-and-claims). | -| `state` | Specify a state that will be validated in an OAuth response. This is usually only provided during redirect flows to obtain an authorization code. Defaults to a random string. | +| `state` | A string that will be passed to `/authorize` endpoint and returned in the OAuth response. The value is used to validate the OAuth response and prevent cross-site request forgery (CSRF). The `state` value passed to [getWithRedirect](#tokengetwithredirectoptions) will be returned along with any requested tokens from [parseFromUrl](#tokenparsefromurloptions). Your app can use this string to perform additional validation and/or pass information from the login page. Defaults to a random string. | | `nonce` | Specify a nonce that will be validated in an `id_token`. This is usually only provided during redirect flows to obtain an authorization code that will be exchanged for an `id_token`. Defaults to a random string. | +| `idp` | Identity provider to use if there is no Okta Session. | +| `idpScope` | A space delimited list of scopes to be provided to the Social Identity Provider when performing [Social Login](social-login) These scopes are used in addition to the scopes already configured on the Identity Provider. | +| `display` | The display parameter to be passed to the Social Identity Provider when performing [Social Login](social-login). | +| `prompt` | Determines whether the Okta login will be displayed on failure. Use `none` to prevent this behavior. Valid values: `none`, `consent`, `login`, or `consent login`. See [Parameter details](https://developer.okta.com/docs/reference/api/oidc/#parameter-details) for more information. | +| `maxAge` | Allowable elapsed time, in seconds, since the last time the end user was actively authenticated by Okta. | +| `loginHint` | A username to prepopulate if prompting for authentication. | -For a list of all available parameters that can be passed to the `/authorize` endpoint, see Okta's [Authorize Request API](https://developer.okta.com/docs/api/resources/oidc#request-parameters). +For more details, see Okta's [Authorize Request API](https://developer.okta.com/docs/api/resources/oidc#request-parameters). ##### Example @@ -1534,43 +1562,52 @@ authClient.token.getWithoutPrompt({ // Use a custom IdP for social authentication idp: '0oa62b57p7c8PaGpU0h7' }) -.then(function(tokenOrTokens) { - // manage token or tokens +.then(function(res) { + var tokens = res.tokens; + + // Do something with tokens, such as + authClient.tokenManager.add('idToken', tokens.idToken); }) .catch(function(err) { // handle OAuthError }); ``` -#### `token.getWithoutPrompt(oauthOptions)` +#### `token.getWithoutPrompt(options)` When you've obtained a sessionToken from the authorization flows, or a session already exists, you can obtain a token or tokens without prompting the user to log in. -* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options) +* `options` - See [Authorize options](#authorize-options) ```javascript authClient.token.getWithoutPrompt({ responseType: 'id_token', // or array of types sessionToken: 'testSessionToken' // optional if the user has an existing Okta session }) -.then(function(tokenOrTokens) { - // manage token or tokens +.then(function(res) { + var tokens = res.tokens; + + // Do something with tokens, such as + authClient.tokenManager.add('idToken', tokens.idToken); }) .catch(function(err) { // handle OAuthError }); ``` -#### `token.getWithPopup(oauthOptions)` +#### `token.getWithPopup(options)` Create token with a popup. -* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options) +* `options` - See [Authorize options](#authorize-options) ```javascript -authClient.token.getWithPopup(oauthOptions) -.then(function(tokenOrTokens) { - // manage token or tokens +authClient.token.getWithPopup(options) +.then(function(res) { + var tokens = res.tokens; + + // Do something with tokens, such as + authClient.tokenManager.add('idToken', tokens.idToken); }) .catch(function(err) { // handle OAuthError @@ -1579,33 +1616,45 @@ authClient.token.getWithPopup(oauthOptions) #### `token.getWithRedirect(options)` -Create token using a redirect. After a successful authentication, the browser will be redirected to the configured [redirectUri](#additional-options). The authorization code, access, or ID Tokens will be available as parameters appended to this URL. By default, values will be in the hash fragment of the URL (for SPA applications) or in the search query (for Web applications). SPA Applications using the PKCE flow can opt to receive the authorization code in the search query by setting the [responseMode](#additional-options) option to "query". +Create token using a redirect. After a successful authentication, the browser will be redirected to the configured [redirectUri](#additional-options). The authorization code, access, or ID Tokens will be available as parameters appended to this URL. Values will be returned in either the search query or hash fragment portion of the URL depending on the [responseMode](#responsemode) -* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options) +* `options` - See [Authorize options](#authorize-options) ```javascript -authClient.token.getWithRedirect(oauthOptions); +authClient.token.getWithRedirect({ + responseType: ['token', 'id_token'], + state: 'any-string-you-want-to-pass-to-callback' // will be URI encoded +}); ``` #### `token.parseFromUrl(options)` -Parses the authorization code, access, or ID Tokens from the URL after a successful authentication redirect. By default, values will be read from the hash fragment of the URL. SPA Applications using the PKCE flow can opt to receive the authorization code in the search query by setting the [responseMode](#additional-options) option to "query". +Parses the authorization code, access, or ID Tokens from the URL after a successful authentication redirect. Values are parsed from either the search query or hash fragment portion of the URL depending on the [responseMode](#responsemode). If an authorization code is present, it will be exchanged for token(s) by posting to the `tokenUrl` endpoint. The ID token will be [verified and validated](https://github.com/okta/okta-auth-js/blob/master/lib/token.js#L186-L190) before available for use. +The `state` string which was passed to `getWithRedirect` will be also be available on the response. + ```javascript authClient.token.parseFromUrl() -.then(function(tokenOrTokens) { +.then(function(res) { + var state = res.state; // passed to getWithRedirect(), can be any string + // manage token or tokens + var tokens = res.tokens; + + // Do something with tokens, such as + authClient.tokenManager.add('idToken', tokens.idToken); + authClient.tokenManager.add('accessToken', tokens.accesstoken); }) .catch(function(err) { // handle OAuthError }); ``` -After reading values, this method will rewrite either the hash fragment or search query portion of the URL (if [responseMode](#additional-options) is "query") so that the code or tokens are no longer present or visible to the user. For this reason, it is recommended to use a dedicated route or path for the [redirectUri](#additional-options) so that this URL rewrite does not interfere with other URL parameters which may be used by your application. A complete login flow will usually save the current URL before calling `getWithRedirect` and restore the URL after saving tokens from `parseFromUrl`. +After reading values, this method will rewrite either the hash fragment or search query portion of the URL (depending on the [responseMode](#responsemode)) so that the code or tokens are no longer present or visible to the user. For this reason, it is recommended to use a dedicated route or path for the [redirectUri](#additional-options) so that this URL rewrite does not interfere with other URL parameters which may be used by your application. A complete login flow will usually save the current URL before calling `getWithRedirect` and restore the URL after saving tokens from `parseFromUrl`. ```javascript // On any page while unauthenticated. Begin login flow @@ -1622,9 +1671,9 @@ authClient.token.getWithRedirect({ ```javascript // On callback (redirectUri) page authClient.token.parseFromUrl() -.then(function(token) { +.then(function(res) { // Save token - authClient.tokenManager.add('accessToken', token); + authClient.tokenManager.add('accessToken', res.tokens.accessToken); // Read saved URL from storage const url = sessionStorage.getItem('url'); @@ -1675,14 +1724,30 @@ authClient.token.renew(tokenToRenew) }); ``` -#### `token.getUserInfo(accessTokenObject)` +#### `token.getUserInfo(accessTokenObject, idTokenObject)` Retrieve the [details about a user](https://developer.okta.com/docs/api/resources/oidc#response-example-success). -* `accessTokenObject` - an access token returned by this library. note: this is not the raw access token +* `accessTokenObject` - (optional) an access token returned by this library. **Note**: this is not the raw access token. +* `idTokenObject` - (optional) an ID token returned by this library. **Note**: this is not the raw ID token. + +By default, if no parameters are passed, both the access token and ID token objects will be retrieved from the TokenManager. If either token has expired it will be renewed automatically by the TokenManager before the user info is requested. It is assumed that the access token is stored using the key "accessToken" and the ID token is stored under the key "idToken". If you have stored either token in a non-standard location, this logic can be skipped by passing the access and ID token objects directly. + + +```javascript +// access and ID tokens are retrieved automatically from the TokenManager +authClient.token.getUserInfo() +.then(function(user) { + // user has details about the user +}); +``` ```javascript -authClient.token.getUserInfo(accessTokenObject) +// In this example, the access token is stored under the key 'myAccessToken' +const accessTokenObject = authClient.tokenManager.get('myAccessToken'); +// In this example, the ID token is stored under the key "myIdToken" +const idTokenObject = authClient.tokenManager.get('myIdToken'); +authClient.token.getUserInfo(accessTokenObject, idTokenObject) .then(function(user) { // user has details about the user }); @@ -1722,8 +1787,8 @@ After receiving an `access_token` or `id_token`, add it to the `tokenManager` to ```javascript authClient.token.getWithPopup() -.then(function(idToken) { - authClient.tokenManager.add('idToken', idToken); +.then(function(res) { + authClient.tokenManager.add('idToken', res.tokens.idToken); }); ``` @@ -1931,3 +1996,4 @@ We're happy to accept contributions and PRs! Please see the [contribution guide] [lang-landing]: https://developer.okta.com/code/javascript [github-issues]: https://github.com/okta/okta-auth-js/issues [github-releases]: https://github.com/okta/okta-auth-js/releases +[social-login]: https://developer.okta.com/docs/concepts/social-login/ diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 1d39348d1..1c78bdebe 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -219,46 +219,6 @@ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 0. You just DO WHAT THE FUCK YOU WANT TO. -q.js -Version (if any): -Brief Description: If a function cannot return a value or throw an -exception without blocking, it can return a promise instead. A promise is an -object that represents the return value or the thrown exception that the -function may eventually provide. A promise can also be used as a proxy for a -remote object to overcome latency. On the first pass, promises can mitigate the -“Pyramid of Doom”: the situation where code marches to the right faster than it -marches forward. -License MIT - -Copyright 2009–2014 Kristopher Michael Kowal. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -reqwest -Version (if any): 2.0.5 -Brief Description: It's AJAX All over again. Includes support for -xmlHttpRequest, JSONP, CORS, and CommonJS Promises A. -License MIT - -Copyright 2015 Dustin Diaz. All rights reserved. - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to diff --git a/package.json b/package.json index 9fdd49061..ab6f7922f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@babel/preset-env": "^7.6.3", "@babel/register": "^7.6.2", "dotenv": "^8.1.0", + "eslint-plugin-compat": "^3.3.0", "eslint-plugin-jasmine": "^2.10.1", "globby": "^6.1.0", "lerna": "^2.11.0" @@ -23,6 +24,7 @@ "test:e2e": "yarn --cwd test/e2e start", "test:browser": "yarn workspace @okta/okta-auth-js test:browser", "test:server": "yarn workspace @okta/okta-auth-js test:server", + "test:karma": "yarn workspace @okta/okta-auth-js test:karma", "test:unit": "yarn workspace @okta/okta-auth-js test", "test:report": "yarn test:unit --ci --silent || true", "prepare": "yarn build", diff --git a/packages/okta-auth-js/.eslintrc.json b/packages/okta-auth-js/.eslintrc.json new file mode 100644 index 000000000..464b535c0 --- /dev/null +++ b/packages/okta-auth-js/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "extends": ["plugin:compat/recommended"], + "settings": { + "polyfills": [ + "Promise", + "Array.from", + "TextEncoder" + ] + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2017 + } +} diff --git a/packages/okta-auth-js/jest.server.js b/packages/okta-auth-js/jest.server.js index d309631bb..b60bfecfc 100644 --- a/packages/okta-auth-js/jest.server.js +++ b/packages/okta-auth-js/jest.server.js @@ -1,5 +1,4 @@ -var packageJson = require('./package.json'); -var OktaAuth = '/' + packageJson.main; +var OktaAuth = '/lib/server/serverIndex.js'; module.exports = { 'coverageDirectory': '/build2/reports/coverage', @@ -12,6 +11,8 @@ module.exports = { ], 'testPathIgnorePatterns': [ './test/spec/browser.js', + './test/spec/browserStorage.js', + './test/spec/cookies.js', './test/spec/fingerprint.js', './test/spec/general.js', './test/spec/oauthUtil.js', diff --git a/packages/okta-auth-js/jquery/index.js b/packages/okta-auth-js/jquery/index.js deleted file mode 100644 index af163ea94..000000000 --- a/packages/okta-auth-js/jquery/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -var jqueryRequest = require('./jqueryRequest'); -var storageUtil = require('../lib/browser/browserStorage'); - -module.exports = require('../lib/browser/browser')(storageUtil, jqueryRequest); diff --git a/packages/okta-auth-js/jquery/jqueryRequest.js b/packages/okta-auth-js/jquery/jqueryRequest.js deleted file mode 100644 index 59b1d6691..000000000 --- a/packages/okta-auth-js/jquery/jqueryRequest.js +++ /dev/null @@ -1,37 +0,0 @@ -/*! - * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - */ - -var $ = require('jquery'); - -function jqueryRequest(method, url, args) { - // TODO: support content-type - var deferred = $.Deferred(); - $.ajax({ - type: method, - url: url, - headers: args.headers, - data: JSON.stringify(args.data), - xhrFields: { - withCredentials: args.withCredentials - } - }) - .then(function(data, textStatus, jqXHR) { - delete jqXHR.then; - deferred.resolve(jqXHR); - }, function(jqXHR) { - delete jqXHR.then; - deferred.reject(jqXHR); - }); - return deferred; -} - -module.exports = jqueryRequest; diff --git a/packages/okta-auth-js/lib/TokenManager.js b/packages/okta-auth-js/lib/TokenManager.js index 43ab5b25e..850bf2a41 100644 --- a/packages/okta-auth-js/lib/TokenManager.js +++ b/packages/okta-auth-js/lib/TokenManager.js @@ -10,12 +10,11 @@ * See the License for the specific language governing permissions and limitations under the License. * */ - +/* global localStorage, sessionStorage */ /* eslint complexity:[0,8] max-statements:[0,21] */ var util = require('./util'); var AuthSdkError = require('./errors/AuthSdkError'); var storageUtil = require('./browser/browserStorage'); -var Q = require('q'); var constants = require('./constants'); var storageBuilder = require('./storageBuilder'); var SdkClock = require('./clock'); @@ -115,7 +114,7 @@ function get(storage, key) { } function getAsync(sdk, tokenMgmtRef, storage, key) { - return Q.Promise(function(resolve) { + return new Promise(function(resolve) { var token = get(storage, key); if (!token || !hasExpired(tokenMgmtRef, token)) { return resolve(token); @@ -152,7 +151,7 @@ function renew(sdk, tokenMgmtRef, storage, key) { throw new AuthSdkError('The tokenManager has no token for the key: ' + key); } } catch (e) { - return Q.reject(e); + return Promise.reject(e); } // Remove existing autoRenew timeout for this key @@ -160,15 +159,7 @@ function renew(sdk, tokenMgmtRef, storage, key) { // Store the renew promise state, to avoid renewing again tokenMgmtRef.renewPromise[key] = sdk.token.renew(token) - .then(function(freshTokens) { - var freshToken = freshTokens; - // With PKCE flow we will receive multiple tokens. Find the one we are looking for - if (freshTokens instanceof Array) { - freshToken = freshTokens.find(function(freshToken) { - return (freshToken.idToken && token.idToken) || (freshToken.accessToken && token.accessToken); - }); - } - + .then(function(freshToken) { var oldToken = get(storage, key); if (!oldToken) { // It is possible to enter a state where the tokens have been cleared @@ -228,7 +219,10 @@ function TokenManager(sdk, options) { storageProvider = sessionStorage; break; case 'cookie': - storageProvider = storageUtil.getCookieStorage(options); + storageProvider = storageUtil.getCookieStorage({ + secure: sdk.options.secureCookies, + sameSite: 'none' + }); break; case 'memory': storageProvider = storageUtil.getInMemoryStorage(); diff --git a/packages/okta-auth-js/lib/browser/browser.js b/packages/okta-auth-js/lib/browser/browser.js index 80f9ba15b..63057aeeb 100644 --- a/packages/okta-auth-js/lib/browser/browser.js +++ b/packages/okta-auth-js/lib/browser/browser.js @@ -13,9 +13,7 @@ /* eslint-disable max-statements */ /* SDK_VERSION is defined in webpack config */ /* global SDK_VERSION */ - -require('../vendor/polyfills'); - +/* global window, navigator, document, crypto */ var Emitter = require('tiny-emitter'); var AuthSdkError = require('../errors/AuthSdkError'); var builderUtil = require('../builderUtil'); @@ -23,7 +21,6 @@ var constants = require('../constants'); var cookies = require('./browserStorage').storage; var http = require('../http'); var oauthUtil = require('../oauthUtil'); -var Q = require('q'); var session = require('../session'); var token = require('../token'); var TokenManager = require('../TokenManager'); @@ -33,11 +30,21 @@ var util = require('../util'); function OktaAuthBuilder(args) { var sdk = this; - var url = builderUtil.getValidUrl(args); - // OKTA-242989: support for grantType will be removed in 3.0 - var usePKCE = args.pkce || args.grantType === 'authorization_code'; + builderUtil.assertValidConfig(args); + + var secureCookies = true; + if (args.secureCookies === false || (sdk.features.isLocalhost() && !sdk.features.isHTTPS())) { + secureCookies = false; + } + if (secureCookies && !sdk.features.isHTTPS()) { + throw new AuthSdkError( + 'The current page is not being served with the HTTPS protocol.\n' + + 'For security reasons, we strongly recommend using HTTPS.\n' + + 'If you cannot use HTTPS, set the "secureCookies" option to false.' + ); + } + this.options = { - url: util.removeTrailingSlash(url), clientId: args.clientId, issuer: util.removeTrailingSlash(args.issuer), authorizeUrl: util.removeTrailingSlash(args.authorizeUrl), @@ -45,7 +52,7 @@ function OktaAuthBuilder(args) { tokenUrl: util.removeTrailingSlash(args.tokenUrl), revokeUrl: util.removeTrailingSlash(args.revokeUrl), logoutUrl: util.removeTrailingSlash(args.logoutUrl), - pkce: usePKCE, + pkce: args.pkce === false ? false : true, redirectUri: args.redirectUri, postLogoutRedirectUri: args.postLogoutRedirectUri, responseMode: args.responseMode, @@ -54,6 +61,7 @@ function OktaAuthBuilder(args) { transformErrorXHR: args.transformErrorXHR, headers: args.headers, onSessionExpired: args.onSessionExpired, + secureCookies }; if (this.options.pkce && !sdk.features.isPKCESupported()) { @@ -201,6 +209,9 @@ proto.features.isHTTPS = function() { return window.location.protocol === 'https:'; }; +proto.features.isLocalhost = function() { + return window.location.hostname === 'localhost'; +}; // { username, password, (relayState), (context) } proto.signIn = function (opts) { var sdk = this; @@ -222,105 +233,93 @@ proto.signIn = function (opts) { }); }; -// Ends the current application session, clearing all local tokens -// Optionally revokes the access token -// Ends the user's Okta session using the API or redirect method -proto.signOut = function (options) { +// Ends the current Okta SSO session without redirecting to Okta. +proto.closeSession = function closeSession() { + var sdk = this; + + // Clear all local tokens + sdk.tokenManager.clear(); + + return sdk.session.close() // DELETE /api/v1/sessions/me + .catch(function(e) { + if (e.name === 'AuthApiError' && e.errorCode === 'E0000007') { + // Session does not exist or has already been closed + return; + } + throw e; + }); +}; + +// Revokes the access token for the application session +proto.revokeAccessToken = async function revokeAccessToken(accessToken) { + var sdk = this; + if (!accessToken) { + accessToken = await sdk.tokenManager.get('accessToken'); + } + // Access token may have been removed. In this case, we will silently succeed. + if (!accessToken) { + return Promise.resolve(); + } + return sdk.token.revoke(accessToken); +}; + +// Revokes accessToken, clears all local tokens, then redirects to Okta to end the SSO session. +proto.signOut = async function (options) { options = util.extend({}, options); // postLogoutRedirectUri must be whitelisted in Okta Admin UI - var postLogoutRedirectUri = options.postLogoutRedirectUri || this.options.postLogoutRedirectUri; + var defaultUri = window.location.origin; + var postLogoutRedirectUri = options.postLogoutRedirectUri + || this.options.postLogoutRedirectUri + || defaultUri; var accessToken = options.accessToken; - var revokeAccessToken = options.revokeAccessToken; + var revokeAccessToken = options.revokeAccessToken !== false; var idToken = options.idToken; var sdk = this; var logoutUrl = oauthUtil.getOAuthUrls(sdk).logoutUrl; - function getAccessToken() { - return new Q() - .then(function() { - if (revokeAccessToken && typeof accessToken === 'undefined') { - return sdk.tokenManager.get('token'); - } - return accessToken; - }); + if (typeof idToken === 'undefined') { + idToken = await sdk.tokenManager.get('idToken'); } - function getIdToken() { - return new Q() - .then(function() { - if (postLogoutRedirectUri && typeof idToken === 'undefined') { - return sdk.tokenManager.get('idToken'); - } - return idToken; - }); + if (revokeAccessToken && typeof accessToken === 'undefined') { + accessToken = await sdk.tokenManager.get('token'); } - function closeSession() { - return sdk.session.close() // DELETE /api/v1/sessions/me - .catch(function(e) { - if (e.name === 'AuthApiError') { - // Most likely cause is session does not exist or has already been closed - // Could also be a network error. Nothing we can do here. - return; - } - throw e; - }); + // Clear all local tokens + sdk.tokenManager.clear(); + + if (revokeAccessToken && accessToken) { + await sdk.revokeAccessToken(accessToken); } - return Q.allSettled([getAccessToken(), getIdToken()]) - .then(function(tokens) { - accessToken = tokens[0].value; - idToken = tokens[1].value; - - // Clear all local tokens - sdk.tokenManager.clear(); - - if (revokeAccessToken && accessToken) { - return sdk.token.revoke(accessToken) - .catch(function(e) { - if (e.name === 'AuthApiError') { - // Capture and ignore network errors - return; - } - throw e; - }); - } - }) + // No idToken? This can happen if the storage was cleared. + // Fallback to XHR signOut, then redirect to the post logout uri + if (!idToken) { + return sdk.closeSession() // can throw if the user cannot be signed out .then(function() { - // XHR signOut method - if (!postLogoutRedirectUri) { - return closeSession(); + if (postLogoutRedirectUri === defaultUri) { + window.location.reload(); + } else { + window.location.assign(postLogoutRedirectUri); } + }); + } - // No idToken? This can happen if the storage was cleared. - // Fallback to XHR signOut, then redirect to the post logout uri - if (!idToken) { - return closeSession() - .catch(function(err) { - // eslint-disable-next-line no-console - console.log('Unhandled exception while closing session', err); - }) - .then(function() { - window.location.assign(postLogoutRedirectUri); - }); - } + // logout redirect using the idToken. + var state = options.state; + var idTokenHint = idToken.idToken; // a string + var logoutUri = logoutUrl + '?id_token_hint=' + encodeURIComponent(idTokenHint) + + '&post_logout_redirect_uri=' + encodeURIComponent(postLogoutRedirectUri); - // logout redirect using the idToken. - var state = options.state; - var idTokenHint = idToken.idToken; // a string - var logoutUri = logoutUrl + '?id_token_hint=' + encodeURIComponent(idTokenHint) + - '&post_logout_redirect_uri=' + encodeURIComponent(postLogoutRedirectUri); - - // State allows option parameters to be passed to logout redirect uri - if (state) { - logoutUri += '&state=' + encodeURIComponent(state); - } - - window.location.assign(logoutUri); - }); + // State allows option parameters to be passed to logout redirect uri + if (state) { + logoutUri += '&state=' + encodeURIComponent(state); + } + + window.location.assign(logoutUri); }; builderUtil.addSharedPrototypes(proto); @@ -340,45 +339,48 @@ proto.fingerprint = function(options) { options = options || {}; var sdk = this; if (!sdk.features.isFingerprintSupported()) { - return Q.reject(new AuthSdkError('Fingerprinting is not supported on this device')); + return Promise.reject(new AuthSdkError('Fingerprinting is not supported on this device')); } - var deferred = Q.defer(); - - var iframe = document.createElement('iframe'); - iframe.style.display = 'none'; + var timeout; + var iframe; + var listener; + var promise = new Promise(function (resolve, reject) { + iframe = document.createElement('iframe'); + iframe.style.display = 'none'; - function listener(e) { - if (!e || !e.data || e.origin !== sdk.options.url) { - return; - } + listener = function listener(e) { + if (!e || !e.data || e.origin !== sdk.getIssuerOrigin()) { + return; + } - try { - var msg = JSON.parse(e.data); - } catch (err) { - return deferred.reject(new AuthSdkError('Unable to parse iframe response')); - } + try { + var msg = JSON.parse(e.data); + } catch (err) { + return reject(new AuthSdkError('Unable to parse iframe response')); + } - if (!msg) { return; } - if (msg.type === 'FingerprintAvailable') { - return deferred.resolve(msg.fingerprint); - } - if (msg.type === 'FingerprintServiceReady') { - e.source.postMessage(JSON.stringify({ - type: 'GetFingerprint' - }), e.origin); - } - } - oauthUtil.addListener(window, 'message', listener); + if (!msg) { return; } + if (msg.type === 'FingerprintAvailable') { + return resolve(msg.fingerprint); + } + if (msg.type === 'FingerprintServiceReady') { + e.source.postMessage(JSON.stringify({ + type: 'GetFingerprint' + }), e.origin); + } + }; + oauthUtil.addListener(window, 'message', listener); - iframe.src = sdk.options.url + '/auth/services/devicefingerprint'; - document.body.appendChild(iframe); + iframe.src = sdk.getIssuerOrigin() + '/auth/services/devicefingerprint'; + document.body.appendChild(iframe); - var timeout = setTimeout(function() { - deferred.reject(new AuthSdkError('Fingerprinting timed out')); - }, options.timeout || 15000); + timeout = setTimeout(function() { + reject(new AuthSdkError('Fingerprinting timed out')); + }, options.timeout || 15000); + }); - return deferred.promise.fin(function() { + return promise.finally(function() { clearTimeout(timeout); oauthUtil.removeListener(window, 'message', listener); if (document.body.contains(iframe)) { diff --git a/packages/okta-auth-js/lib/browser/browserIndex.js b/packages/okta-auth-js/lib/browser/browserIndex.js index 91822a4a8..7ec3a6476 100644 --- a/packages/okta-auth-js/lib/browser/browserIndex.js +++ b/packages/okta-auth-js/lib/browser/browserIndex.js @@ -11,7 +11,7 @@ * */ -var fetchRequest = require('../../fetch/fetchRequest'); +var fetchRequest = require('../fetch/fetchRequest'); var storageUtil = require('./browserStorage'); module.exports = require('./browser')(storageUtil, fetchRequest); diff --git a/packages/okta-auth-js/lib/browser/browserStorage.js b/packages/okta-auth-js/lib/browser/browserStorage.js index b2c36d9ea..5235eb086 100644 --- a/packages/okta-auth-js/lib/browser/browserStorage.js +++ b/packages/okta-auth-js/lib/browser/browserStorage.js @@ -10,10 +10,11 @@ * See the License for the specific language governing permissions and limitations under the License. * */ - +/* global localStorage, sessionStorage */ var Cookies = require('js-cookie'); var storageBuilder = require('../storageBuilder'); var constants = require('../constants'); +var AuthSdkError = require('../errors/AuthSdkError'); // Building this as an object allows us to mock the functions in our tests var storageUtil = {}; @@ -38,23 +39,23 @@ storageUtil.browserHasSessionStorage = function() { } }; -storageUtil.getPKCEStorage = function() { +storageUtil.getPKCEStorage = function(options) { if (storageUtil.browserHasLocalStorage()) { return storageBuilder(storageUtil.getLocalStorage(), constants.PKCE_STORAGE_NAME); } else if (storageUtil.browserHasSessionStorage()) { return storageBuilder(storageUtil.getSessionStorage(), constants.PKCE_STORAGE_NAME); } else { - return storageBuilder(storageUtil.getCookieStorage(), constants.PKCE_STORAGE_NAME); + return storageBuilder(storageUtil.getCookieStorage(options), constants.PKCE_STORAGE_NAME); } }; -storageUtil.getHttpCache = function() { +storageUtil.getHttpCache = function(options) { if (storageUtil.browserHasLocalStorage()) { return storageBuilder(storageUtil.getLocalStorage(), constants.CACHE_STORAGE_NAME); } else if (storageUtil.browserHasSessionStorage()) { return storageBuilder(storageUtil.getSessionStorage(), constants.CACHE_STORAGE_NAME); } else { - return storageBuilder(storageUtil.getCookieStorage(), constants.CACHE_STORAGE_NAME); + return storageBuilder(storageUtil.getCookieStorage(options), constants.CACHE_STORAGE_NAME); } }; @@ -68,9 +69,11 @@ storageUtil.getSessionStorage = function() { // Provides webStorage-like interface for cookies storageUtil.getCookieStorage = function(options) { - options = options || {}; - var secure = options.secure; // currently opt-in - var sameSite = options.sameSite || 'none'; + const secure = options.secure; + const sameSite = options.sameSite; + if (typeof secure === 'undefined' || typeof sameSite === 'undefined') { + throw new AuthSdkError('getCookieStorage: "secure" and "sameSite" options must be provided'); + } return { getItem: storageUtil.storage.get, setItem: function(key, value) { @@ -109,11 +112,15 @@ storageUtil.testStorage = function(storage) { storageUtil.storage = { set: function(name, value, expiresAt, options) { - options = options || {}; + const secure = options.secure; + const sameSite = options.sameSite; + if (typeof secure === 'undefined' || typeof sameSite === 'undefined') { + throw new AuthSdkError('storage.set: "secure" and "sameSite" options must be provided'); + } var cookieOptions = { path: options.path || '/', - secure: options.secure, - sameSite: options.sameSite + secure, + sameSite }; // eslint-disable-next-line no-extra-boolean-cast diff --git a/packages/okta-auth-js/lib/builderUtil.js b/packages/okta-auth-js/lib/builderUtil.js index 963b5fe98..7c9235ef0 100644 --- a/packages/okta-auth-js/lib/builderUtil.js +++ b/packages/okta-auth-js/lib/builderUtil.js @@ -14,33 +14,37 @@ var AuthSdkError = require('./errors/AuthSdkError'); var tx = require('./tx'); var util = require('./util'); -function getValidUrl(args) { +// TODO: use @okta/configuration-validation (move module to this monorepo?) +function assertValidConfig(args) { if (!args) { throw new AuthSdkError('No arguments passed to constructor. ' + 'Required usage: new OktaAuth(args)'); } - var url = args.url; - if (!url) { - var isUrlRegex = new RegExp('^http?s?://.+'); - if (args.issuer && isUrlRegex.test(args.issuer)) { - // Infer the URL from the issuer URL, omitting the /oauth2/{authServerId} - url = args.issuer.split('/oauth2/')[0]; - } else { - throw new AuthSdkError('No url passed to constructor. ' + - 'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com"})'); - } + var issuer = args.issuer; + if (!issuer) { + throw new AuthSdkError('No issuer passed to constructor. ' + + 'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})'); } - if (url.indexOf('-admin.') !== -1) { - throw new AuthSdkError('URL passed to constructor contains "-admin" in subdomain. ' + - 'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com})'); + var isUrlRegex = new RegExp('^http?s?://.+'); + if (!isUrlRegex.test(args.issuer)) { + throw new AuthSdkError('Issuer must be a valid URL. ' + + 'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})'); } - return url; + if (issuer.indexOf('-admin.') !== -1) { + throw new AuthSdkError('Issuer URL passed to constructor contains "-admin" in subdomain. ' + + 'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com})'); + } } function addSharedPrototypes(proto) { + proto.getIssuerOrigin = function() { + // Infer the URL from the issuer URL, omitting the /oauth2/{authServerId} + return this.options.issuer.split('/oauth2/')[0]; + }; + // { username, (relayState) } proto.forgotPassword = function (opts) { return tx.postToTransaction(this, '/api/v1/authn/recovery/password', opts); @@ -90,5 +94,5 @@ function buildOktaAuth(OktaAuthBuilder) { module.exports = { addSharedPrototypes: addSharedPrototypes, buildOktaAuth: buildOktaAuth, - getValidUrl: getValidUrl + assertValidConfig: assertValidConfig }; diff --git a/packages/okta-auth-js/lib/crypto.js b/packages/okta-auth-js/lib/crypto.js index c4ca2a3ed..9608549eb 100644 --- a/packages/okta-auth-js/lib/crypto.js +++ b/packages/okta-auth-js/lib/crypto.js @@ -9,9 +9,20 @@ * * See the License for the specific language governing permissions and limitations under the License. */ - +/* global crypto, Uint8Array, TextEncoder */ var util = require('./util'); +function getOidcHash(str) { + var buffer = new TextEncoder().encode(str); + return crypto.subtle.digest('SHA-256', buffer).then(function(arrayBuffer) { + var intBuffer = new Uint8Array(arrayBuffer); + var firstHalf = intBuffer.slice(0, 16); + var hash = String.fromCharCode.apply(null, firstHalf); + var b64u = util.stringToBase64Url(hash); // url-safe base64 variant + return b64u; + }); +} + function verifyToken(idToken, key) { key = util.clone(key); @@ -51,5 +62,6 @@ function verifyToken(idToken, key) { } module.exports = { + getOidcHash: getOidcHash, verifyToken: verifyToken }; diff --git a/packages/okta-auth-js/fetch/fetchRequest.js b/packages/okta-auth-js/lib/fetch/fetchRequest.js similarity index 58% rename from packages/okta-auth-js/fetch/fetchRequest.js rename to packages/okta-auth-js/lib/fetch/fetchRequest.js index 15ea92396..2dee79a00 100644 --- a/packages/okta-auth-js/fetch/fetchRequest.js +++ b/packages/okta-auth-js/lib/fetch/fetchRequest.js @@ -12,6 +12,36 @@ var fetch = require('cross-fetch'); + +function readData(response) { + if (response.headers.get('Content-Type') && + response.headers.get('Content-Type').toLowerCase().indexOf('application/json') >= 0) { + return response.json() + // JSON parse can fail if response is not a valid object + .catch(e => { + return { + error: e, + errorSummary: 'Could not parse server response' + }; + }); + } else { + return response.text(); + } +} + +function formatResult(status, data) { + const isObject = typeof data === 'object'; + const result = { + responseText: isObject ? JSON.stringify(data) : data, + status: status + }; + if (isObject) { + result.responseType = 'json'; + result.responseJSON = data; + } + return result; +} + /* eslint-disable complexity */ function fetchRequest(method, url, args) { var body = args.data; @@ -32,23 +62,17 @@ function fetchRequest(method, url, args) { .then(function(response) { var error = !response.ok; var status = response.status; - var respHandler = function(resp) { - var result = { - responseText: resp, - status: status - }; - if (error) { - // Throwing response object since error handling is done in http.js - throw result; - } - return result; - }; - if (response.headers.get('Content-Type') && - response.headers.get('Content-Type').toLowerCase().indexOf('application/json') >= 0) { - return response.json().then(respHandler); - } else { - return response.text().then(respHandler); - } + return readData(response) + .then(data => { + return formatResult(status, data); + }) + .then(result => { + if (error) { + // Throwing result object since error handling is done in http.js + throw result; + } + return result; + }); }); return fetchPromise; } diff --git a/packages/okta-auth-js/lib/http.js b/packages/okta-auth-js/lib/http.js index c7edf6794..d99aa15e2 100644 --- a/packages/okta-auth-js/lib/http.js +++ b/packages/okta-auth-js/lib/http.js @@ -13,7 +13,6 @@ /* eslint-disable complexity */ var util = require('./util'); -var Q = require('q'); var AuthApiError = require('./errors/AuthApiError'); var constants = require('./constants'); @@ -27,13 +26,16 @@ function httpRequest(sdk, options) { withCredentials = options.withCredentials !== false, // default value is true storageUtil = sdk.options.storageUtil, storage = storageUtil.storage, - httpCache = storageUtil.getHttpCache(); + httpCache = storageUtil.getHttpCache({ + secure: sdk.options.secureCookies, + sameSite: 'none' + }); if (options.cacheResponse) { var cacheContents = httpCache.getStorage(); var cachedResponse = cacheContents[url]; if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) { - return Q.resolve(cachedResponse.response); + return Promise.resolve(cachedResponse.response); } } @@ -55,7 +57,7 @@ function httpRequest(sdk, options) { }; var err, res; - return new Q(sdk.options.httpRequestClient(method, url, ajaxOptions)) + return sdk.options.httpRequestClient(method, url, ajaxOptions) .then(function(resp) { res = resp.responseText; if (res && util.isString(res)) { @@ -69,7 +71,10 @@ function httpRequest(sdk, options) { } if (res && res.stateToken && res.expiresAt) { - storage.set(constants.STATE_TOKEN_KEY_NAME, res.stateToken, res.expiresAt); + storage.set(constants.STATE_TOKEN_KEY_NAME, res.stateToken, res.expiresAt, { + secure: sdk.options.secureCookies, + sameSite: 'none' + }); } if (res && options.cacheResponse) { @@ -81,7 +86,7 @@ function httpRequest(sdk, options) { return res; }) - .fail(function(resp) { + .catch(function(resp) { var serverErr = resp.responseText || {}; if (util.isString(serverErr)) { try { @@ -112,7 +117,7 @@ function httpRequest(sdk, options) { } function get(sdk, url, options) { - url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url; + url = util.isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url; var getOptions = { url: url, method: 'GET' @@ -122,7 +127,7 @@ function get(sdk, url, options) { } function post(sdk, url, args, options) { - url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url; + url = util.isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url; var postOptions = { url: url, method: 'POST', diff --git a/packages/okta-auth-js/lib/oauthUtil.js b/packages/okta-auth-js/lib/oauthUtil.js index 2d1ef6c8b..d65838d0b 100644 --- a/packages/okta-auth-js/lib/oauthUtil.js +++ b/packages/okta-auth-js/lib/oauthUtil.js @@ -10,15 +10,13 @@ * See the License for the specific language governing permissions and limitations under the License. * */ - +/* global window, document */ /* eslint-disable complexity, max-statements */ var http = require('./http'); var util = require('./util'); var storageUtil = require('./browser/browserStorage'); var AuthSdkError = require('./errors/AuthSdkError'); -var httpCache = storageUtil.getHttpCache(); - function generateState() { return util.genRandomString(64); } @@ -78,13 +76,18 @@ function loadPopup(src, options) { } function getWellKnown(sdk, issuer) { - var authServerUri = (issuer || sdk.options.issuer || sdk.options.url); + var authServerUri = (issuer || sdk.options.issuer); return http.get(sdk, authServerUri + '/.well-known/openid-configuration', { cacheResponse: true }); } function getKey(sdk, issuer, kid) { + var httpCache = storageUtil.getHttpCache({ + secure: sdk.options.secureCookies, + sameSite: 'none' + }); + return getWellKnown(sdk, issuer) .then(function(wellKnown) { var jwksUri = wellKnown['jwks_uri']; @@ -161,7 +164,10 @@ function validateClaims(sdk, claims, validationParams) { } } -function getOAuthUrls(sdk, oauthParams, options) { +function getOAuthUrls(sdk, options) { + if (arguments.length > 2) { + throw new AuthSdkError('As of version 3.0, "getOAuthUrls" takes only a single set of options'); + } options = options || {}; // Get user-supplied arguments @@ -172,53 +178,7 @@ function getOAuthUrls(sdk, oauthParams, options) { var logoutUrl = util.removeTrailingSlash(options.logoutUrl) || sdk.options.logoutUrl; var revokeUrl = util.removeTrailingSlash(options.revokeUrl) || sdk.options.revokeUrl; - // If an issuer exists but it's not a url, assume it's an authServerId - if (issuer && !(/^https?:/.test(issuer))) { - // Make it a url - issuer = sdk.options.url + '/oauth2/' + issuer; - } - - // If an authorizeUrl is supplied without an issuer, and an id_token is requested - if (!issuer && authorizeUrl && - oauthParams.responseType.indexOf('id_token') !== -1) { - // The issuer is ambiguous, so we won't be able to validate the id_token jwt - throw new AuthSdkError('Cannot request idToken with an authorizeUrl without an issuer'); - } - - // If a token is requested without an issuer - if (!issuer && oauthParams && oauthParams.responseType.indexOf('token') !== -1) { - // If an authorizeUrl is supplied without a userinfoUrl - if (authorizeUrl && !userinfoUrl) { - // The userinfoUrl is ambiguous, so we won't be able to call getUserInfo - throw new AuthSdkError('Cannot request accessToken with an authorizeUrl without an issuer or userinfoUrl'); - } - - // If a userinfoUrl is supplied without a authorizeUrl - if (userinfoUrl && !authorizeUrl) { - // The authorizeUrl is ambiguous, so we won't be able to call the authorize endpoint - throw new AuthSdkError('Cannot request token with an userinfoUrl without an issuer or authorizeUrl'); - } - } - - // Default the issuer to our baseUrl - issuer = issuer || sdk.options.url; - - // Trim trailing slashes - issuer = util.removeTrailingSlash(issuer); - - var baseUrl = issuer; - // A custom auth server issuer looks like: - // https://example.okta.com/oauth2/aus8aus76q8iphupD0h7 - // - // Most orgs have a "default" custom authorization server: - // https://example.okta.com/oauth2/default - var customAuthServerRegex = new RegExp('^https?://.*?/oauth2/.+'); - if (!customAuthServerRegex.test(baseUrl)) { - // Append '/oauth2' if necessary - if (!baseUrl.endsWith('/oauth2')) { - baseUrl += '/oauth2'; - } - } + var baseUrl = issuer.indexOf('/oauth2') > 0 ? issuer : issuer + '/oauth2'; authorizeUrl = authorizeUrl || baseUrl + '/v1/authorize'; userinfoUrl = userinfoUrl || baseUrl + '/v1/userinfo'; diff --git a/packages/okta-auth-js/lib/pkce.js b/packages/okta-auth-js/lib/pkce.js index 90858f776..af497c7c2 100644 --- a/packages/okta-auth-js/lib/pkce.js +++ b/packages/okta-auth-js/lib/pkce.js @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and limitations under the License. * */ - +/* global crypto */ /* eslint-disable complexity, max-statements */ var AuthSdkError = require('./errors/AuthSdkError'); var http = require('./http'); @@ -41,19 +41,26 @@ function generateVerifier(prefix) { return encodeURIComponent(verifier).slice(0, MAX_VERIFIER_LENGTH); } +function getStorage(sdk) { + return sdk.options.storageUtil.getPKCEStorage({ + secure: sdk.options.secureCookies, + sameSite: 'none' + }); +} + function saveMeta(sdk, meta) { - var storage = sdk.options.storageUtil.getPKCEStorage(); + var storage = getStorage(sdk); storage.setStorage(meta); } function loadMeta(sdk) { - var storage = sdk.options.storageUtil.getPKCEStorage(); + var storage = getStorage(sdk); var obj = storage.getStorage(); return obj; } function clearMeta(sdk) { - var storage = sdk.options.storageUtil.getPKCEStorage(); + var storage = getStorage(sdk); storage.clearStorage(); } diff --git a/packages/okta-auth-js/lib/server/server.js b/packages/okta-auth-js/lib/server/server.js index cbd1a4e2f..7b7d5be88 100644 --- a/packages/okta-auth-js/lib/server/server.js +++ b/packages/okta-auth-js/lib/server/server.js @@ -12,8 +12,6 @@ /* eslint-disable complexity */ /* eslint-disable max-statements */ -require('../vendor/polyfills'); - var builderUtil = require('../builderUtil'); var SDK_VERSION = require('../../package.json').version; var storage = require('./serverStorage').storage; @@ -23,9 +21,9 @@ var util = require('../util'); function OktaAuthBuilder(args) { var sdk = this; - var url = builderUtil.getValidUrl(args); + builderUtil.assertValidConfig(args); this.options = { - url: util.removeTrailingSlash(url), + issuer: util.removeTrailingSlash(args.issuer), httpRequestClient: args.httpRequestClient, storageUtil: args.storageUtil, headers: args.headers diff --git a/packages/okta-auth-js/lib/server/serverIndex.js b/packages/okta-auth-js/lib/server/serverIndex.js index e1019bec8..45b633f81 100644 --- a/packages/okta-auth-js/lib/server/serverIndex.js +++ b/packages/okta-auth-js/lib/server/serverIndex.js @@ -11,7 +11,7 @@ * */ -var fetchRequest = require('../../fetch/fetchRequest'); +var fetchRequest = require('../fetch/fetchRequest'); var storageUtil = require('./serverStorage'); module.exports = require('./server')(storageUtil, fetchRequest); diff --git a/packages/okta-auth-js/lib/session.js b/packages/okta-auth-js/lib/session.js index 388daeea0..be4f08e31 100644 --- a/packages/okta-auth-js/lib/session.js +++ b/packages/okta-auth-js/lib/session.js @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and limitations under the License. * */ - +/* global window */ var util = require('./util'); var http = require('./http'); @@ -22,7 +22,7 @@ function sessionExists(sdk) { } return false; }) - .fail(function() { + .catch(function() { return false; }); } @@ -42,7 +42,7 @@ function getSession(sdk) { return res; }) - .fail(function() { + .catch(function() { // Return INACTIVE status on failure return {status: 'INACTIVE'}; }); @@ -50,7 +50,7 @@ function getSession(sdk) { function closeSession(sdk) { return http.httpRequest(sdk, { - url: sdk.options.url + '/api/v1/sessions/me', + url: sdk.getIssuerOrigin() + '/api/v1/sessions/me', method: 'DELETE' }); } @@ -61,7 +61,7 @@ function refreshSession(sdk) { function setCookieAndRedirect(sdk, sessionToken, redirectUrl) { redirectUrl = redirectUrl || window.location.href; - window.location = sdk.options.url + '/login/sessionCookieRedirect' + + window.location = sdk.getIssuerOrigin() + '/login/sessionCookieRedirect' + util.toQueryParams({ checkAccountSetupComplete: true, token: sessionToken, diff --git a/packages/okta-auth-js/lib/storageBuilder.js b/packages/okta-auth-js/lib/storageBuilder.js index 261a8c597..cc9112a93 100644 --- a/packages/okta-auth-js/lib/storageBuilder.js +++ b/packages/okta-auth-js/lib/storageBuilder.js @@ -40,7 +40,7 @@ function storageBuilder(webstorage, storageName) { function clearStorage(key) { if (!key) { - setStorage({}); + return setStorage({}); } var storage = getStorage(); delete storage[key]; diff --git a/packages/okta-auth-js/lib/token.js b/packages/okta-auth-js/lib/token.js index b6442b74d..812699d6b 100644 --- a/packages/okta-auth-js/lib/token.js +++ b/packages/okta-auth-js/lib/token.js @@ -10,12 +10,11 @@ * See the License for the specific language governing permissions and limitations under the License. * */ - +/* global window, document, btoa */ /* eslint-disable complexity, max-statements */ var http = require('./http'); var util = require('./util'); var oauthUtil = require('./oauthUtil'); -var Q = require('q'); var sdkCrypto = require('./crypto'); var AuthSdkError = require('./errors/AuthSdkError'); var OAuthError = require('./errors/OAuthError'); @@ -25,7 +24,7 @@ var PKCE = require('./pkce'); // Only the access token can be revoked in SPA applications function revokeToken(sdk, token) { - return new Q() + return Promise.resolve() .then(function() { if (!token || !token.accessToken) { throw new AuthSdkError('A valid access token object is required'); @@ -70,7 +69,7 @@ function decodeToken(token) { // Verify the id token function verifyToken(sdk, token, validationParams) { - return new Q() + return Promise.resolve() .then(function() { if (!token || !token.idToken) { throw new AuthSdkError('Only idTokens may be verified'); @@ -80,7 +79,7 @@ function verifyToken(sdk, token, validationParams) { var validationOptions = { clientId: sdk.options.clientId, - issuer: sdk.options.issuer || sdk.options.url, + issuer: sdk.options.issuer, ignoreSignature: sdk.options.ignoreSignature }; @@ -103,66 +102,91 @@ function verifyToken(sdk, token, validationParams) { if (!valid) { throw new AuthSdkError('The token signature is not valid'); } + if (validationParams.accessToken && token.claims.at_hash) { + return sdkCrypto.getOidcHash(validationParams.accessToken) + .then(hash => { + if (hash !== token.claims.at_hash) { + throw new AuthSdkError('Token hash verification failed'); + } + }); + } + }) + .then(() => { return token; }); }); } function addPostMessageListener(sdk, timeout, state) { - var deferred = Q.defer(); + var responseHandler; + var timeoutId; + var msgReceivedOrTimeout = new Promise(function(resolve, reject) { + + responseHandler = function responseHandler(e) { + if (!e.data || e.data.state !== state) { + // A message not meant for us + return; + } - function responseHandler(e) { - if (!e.data || e.data.state !== state) { - // A message not meant for us - return; - } + // Configuration mismatch between saved token and current app instance + // This may happen if apps with different issuers are running on the same host url + // If they share the same storage key, they may read and write tokens in the same location. + // Common when developing against http://localhost + if (e.origin !== sdk.getIssuerOrigin()) { + return reject(new AuthSdkError('The request does not match client configuration')); + } - // Configuration mismatch between saved token and current app instance - // This may happen if apps with different issuers are running on the same host url - // If they share the same storage key, they may read and write tokens in the same location. - // Common when developing against http://localhost - if (e.origin !== sdk.options.url) { - return deferred.reject(new AuthSdkError('The request does not match client configuration')); - } + resolve(e.data); + }; - deferred.resolve(e.data); - } + oauthUtil.addListener(window, 'message', responseHandler); - oauthUtil.addListener(window, 'message', responseHandler); + timeoutId = setTimeout(function() { + reject(new AuthSdkError('OAuth flow timed out')); + }, timeout || 120000); + }); - return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out')) - .fin(function() { + return msgReceivedOrTimeout + .finally(function() { + clearTimeout(timeoutId); oauthUtil.removeListener(window, 'message', responseHandler); }); } function addFragmentListener(sdk, windowEl, timeout) { - var deferred = Q.defer(); - - function hashChangeHandler() { - /* - We are only able to access window.location.hash on a window - that has the same domain. A try/catch is necessary because - there's no other way to determine that the popup is in - another domain. When we try to access a window on another - domain, an error is thrown. - */ - try { - if (windowEl && - windowEl.location && - windowEl.location.hash) { - deferred.resolve(oauthUtil.hashToObject(windowEl.location.hash)); - } else if (windowEl && !windowEl.closed) { + var timeoutId; + var promise = new Promise(function(resolve, reject) { + function hashChangeHandler() { + /* + We are only able to access window.location.hash on a window + that has the same domain. A try/catch is necessary because + there's no other way to determine that the popup is in + another domain. When we try to access a window on another + domain, an error is thrown. + */ + try { + if (windowEl && + windowEl.location && + windowEl.location.hash) { + resolve(oauthUtil.hashToObject(windowEl.location.hash)); + } else if (windowEl && !windowEl.closed) { + setTimeout(hashChangeHandler, 500); + } + } catch (err) { setTimeout(hashChangeHandler, 500); } - } catch (err) { - setTimeout(hashChangeHandler, 500); } - } + + hashChangeHandler(); - hashChangeHandler(); + timeoutId = setTimeout(function() { + reject(new AuthSdkError('OAuth flow timed out')); + }, timeout || 120000); + }); - return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out')); + return promise.finally(function() { + clearTimeout(timeoutId); + }); } function exchangeCodeForToken(sdk, oauthParams, authorizationCode, urls) { @@ -180,7 +204,7 @@ function exchangeCodeForToken(sdk, oauthParams, authorizationCode, urls) { validateResponse(res, getTokenParams); return res; }) - .fin(function() { + .finally(function() { PKCE.clearMeta(sdk); }); } @@ -199,10 +223,14 @@ function handleOAuthResponse(sdk, oauthParams, res, urls) { urls = urls || {}; var responseType = oauthParams.responseType; + if (!Array.isArray(responseType)) { + responseType = [responseType]; + } + var scopes = util.clone(oauthParams.scopes); var clientId = oauthParams.clientId || sdk.options.clientId; - return new Q() + return Promise.resolve() .then(function() { validateResponse(res, oauthParams); @@ -215,23 +243,29 @@ function handleOAuthResponse(sdk, oauthParams, res, urls) { return res; }).then(function(res) { var tokenDict = {}; - - if (res['access_token']) { - tokenDict['token'] = { - accessToken: res['access_token'], - expiresAt: Number(res['expires_in']) + Math.floor(Date.now()/1000), - tokenType: res['token_type'], + var expiresIn = res.expires_in; + var tokenType = res.token_type; + var accessToken = res.access_token; + var idToken = res.id_token; + + if (accessToken) { + tokenDict.accessToken = { + value: accessToken, + accessToken: accessToken, + expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), + tokenType: tokenType, scopes: scopes, authorizeUrl: urls.authorizeUrl, userinfoUrl: urls.userinfoUrl }; } - if (res['id_token']) { - var jwt = sdk.token.decode(res['id_token']); + if (idToken) { + var jwt = sdk.token.decode(idToken); - var idToken = { - idToken: res['id_token'], + var idTokenObj = { + value: idToken, + idToken: idToken, claims: jwt.payload, expiresAt: jwt.payload.exp, scopes: scopes, @@ -243,16 +277,17 @@ function handleOAuthResponse(sdk, oauthParams, res, urls) { var validationParams = { clientId: clientId, issuer: urls.issuer, - nonce: oauthParams.nonce + nonce: oauthParams.nonce, + accessToken: accessToken }; if (oauthParams.ignoreSignature !== undefined) { validationParams.ignoreSignature = oauthParams.ignoreSignature; } - return verifyToken(sdk, idToken, validationParams) + return verifyToken(sdk, idTokenObj, validationParams) .then(function() { - tokenDict['id_token'] = idToken; + tokenDict.idToken = idTokenObj; return tokenDict; }); } @@ -260,34 +295,30 @@ function handleOAuthResponse(sdk, oauthParams, res, urls) { return tokenDict; }) .then(function(tokenDict) { - if (!Array.isArray(responseType)) { - return tokenDict[responseType]; + // Validate received tokens against requested response types + if (responseType.indexOf('token') !== -1 && !tokenDict.accessToken) { + // eslint-disable-next-line max-len + throw new AuthSdkError('Unable to parse OAuth flow response: response type "token" was requested but "access_token" was not returned.'); + } + if (responseType.indexOf('id_token') !== -1 && !tokenDict.idToken) { + // eslint-disable-next-line max-len + throw new AuthSdkError('Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.'); } - // Validate response against tokenTypes - var validateTokenTypes = ['token', 'id_token']; - validateTokenTypes.filter(function(key) { - return (responseType.indexOf(key) !== -1); - }).forEach(function(key) { - if (!tokenDict[key]) { - throw new AuthSdkError('Unable to parse OAuth flow response: ' + key + ' was not returned.'); - } - }); - - // Create token array in the order of the responseType array - return responseType.map(function(item) { - return tokenDict[item]; - }); + return { + tokens: tokenDict, + state: res.state + }; }); } function getDefaultOAuthParams(sdk) { return { - pkce: sdk.options.pkce || false, + pkce: sdk.options.pkce, clientId: sdk.options.clientId, redirectUri: sdk.options.redirectUri || window.location.href, - responseType: 'id_token', - responseMode: 'okta_post_message', + responseType: ['token', 'id_token'], + responseMode: sdk.options.responseMode, state: oauthUtil.generateState(), nonce: oauthUtil.generateNonce(), scopes: ['openid', 'email'], @@ -395,11 +426,13 @@ function buildAuthorizeParams(oauthParams) { * @param {String} [options.popupTitle] Title dispayed in the popup. * Defaults to 'External Identity Provider User Authentication' */ -function getToken(sdk, oauthOptions, options) { - oauthOptions = oauthOptions || {}; +function getToken(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getToken" takes only a single set of options')); + } options = options || {}; - return prepareOauthParams(sdk, oauthOptions) + return prepareOauthParams(sdk, options) .then(function(oauthParams) { // Start overriding any options that don't make sense @@ -413,9 +446,9 @@ function getToken(sdk, oauthOptions, options) { display: 'popup' }; - if (oauthOptions.sessionToken) { + if (options.sessionToken) { util.extend(oauthParams, sessionTokenOverrides); - } else if (oauthOptions.idp) { + } else if (options.idp) { util.extend(oauthParams, idpOverrides); } @@ -423,14 +456,11 @@ function getToken(sdk, oauthOptions, options) { var requestUrl, endpoint, urls; - try { - // Get authorizeUrl and issuer - urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); - endpoint = oauthOptions.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; - requestUrl = endpoint + buildAuthorizeParams(oauthParams); - } catch (e) { - return Q.reject(e); - } + + // Get authorizeUrl and issuer + urls = oauthUtil.getOAuthUrls(sdk, oauthParams); + endpoint = options.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; + requestUrl = endpoint + buildAuthorizeParams(oauthParams); // Determine the flow type var flowType; @@ -457,22 +487,22 @@ function getToken(sdk, oauthOptions, options) { .then(function(res) { return handleOAuthResponse(sdk, oauthParams, res, urls); }) - .fin(function() { + .finally(function() { if (document.body.contains(iframeEl)) { iframeEl.parentElement.removeChild(iframeEl); } }); - case 'POPUP': // eslint-disable-line no-case-declarations - var popupPromise; + case 'POPUP': + var oauthPromise; // resolves with OAuth response // Add listener on postMessage before window creation, so // postMessage isn't triggered before we're listening if (oauthParams.responseMode === 'okta_post_message') { if (!sdk.features.isPopupPostMessageSupported()) { - return Q.reject(new AuthSdkError('This browser doesn\'t have full postMessage support')); + throw new AuthSdkError('This browser doesn\'t have full postMessage support'); } - popupPromise = addPostMessageListener(sdk, options.timeout, oauthParams.state); + oauthPromise = addPostMessageListener(sdk, options.timeout, oauthParams.state); } // Create the window @@ -486,92 +516,89 @@ function getToken(sdk, oauthOptions, options) { var windowOrigin = getOrigin(sdk.idToken.authorize._getLocationHref()); var redirectUriOrigin = getOrigin(oauthParams.redirectUri); if (windowOrigin !== redirectUriOrigin) { - return Q.reject(new AuthSdkError('Using fragment, the redirectUri origin (' + redirectUriOrigin + - ') must match the origin of this page (' + windowOrigin + ')')); + throw new AuthSdkError('Using fragment, the redirectUri origin (' + redirectUriOrigin + + ') must match the origin of this page (' + windowOrigin + ')'); } - popupPromise = addFragmentListener(sdk, windowEl, options.timeout); + oauthPromise = addFragmentListener(sdk, windowEl, options.timeout); } - // Both postMessage and fragment require a poll to see if the popup closed - var popupDeferred = Q.defer(); - /* eslint-disable-next-line no-case-declarations, no-inner-declarations */ - function hasClosed(win) { - if (!win || win.closed) { - popupDeferred.reject(new AuthSdkError('Unable to parse OAuth flow response')); - return true; - } - } - var closePoller = setInterval(function() { - if (hasClosed(windowEl)) { + // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window. + var popupPromise = new Promise(function(resolve, reject) { + var closePoller = setInterval(function() { + if (!windowEl || windowEl.closed) { + clearInterval(closePoller); + reject(new AuthSdkError('Unable to parse OAuth flow response')); + } + }, 100); + + // Proxy the OAuth promise results + oauthPromise + .then(function(res) { clearInterval(closePoller); - } - }, 500); - - // Proxy the promise results into the deferred - popupPromise - .then(function(res) { - popupDeferred.resolve(res); - }) - .fail(function(err) { - popupDeferred.reject(err); + resolve(res); + }) + .catch(function(err) { + clearInterval(closePoller); + reject(err); + }); }); - return popupDeferred.promise + return popupPromise .then(function(res) { return handleOAuthResponse(sdk, oauthParams, res, urls); }) - .fin(function() { - clearInterval(closePoller); + .finally(function() { if (windowEl && !windowEl.closed) { windowEl.close(); } }); default: - return Q.reject(new AuthSdkError('The full page redirect flow is not supported')); + throw new AuthSdkError('The full page redirect flow is not supported'); } }); } -function getWithoutPrompt(sdk, oauthOptions, options) { - var oauthParams = util.clone(oauthOptions) || {}; - util.extend(oauthParams, { +function getWithoutPrompt(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getWithoutPrompt" takes only a single set of options')); + } + options = util.clone(options) || {}; + util.extend(options, { prompt: 'none', responseMode: 'okta_post_message', display: null }); - return getToken(sdk, oauthParams, options); + return getToken(sdk, options); } -function getWithPopup(sdk, oauthOptions, options) { - var oauthParams = util.clone(oauthOptions) || {}; - util.extend(oauthParams, { +function getWithPopup(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getWithPopup" takes only a single set of options')); + } + options = util.clone(options) || {}; + util.extend(options, { display: 'popup', responseMode: 'okta_post_message' }); - return getToken(sdk, oauthParams, options); + return getToken(sdk, options); } -function prepareOauthParams(sdk, oauthOptions) { +function prepareOauthParams(sdk, options) { // clone and prepare options - oauthOptions = util.clone(oauthOptions) || {}; - - // OKTA-242989: support for grantType will be removed in 3.0 - if (oauthOptions.grantType === 'authorization_code') { - oauthOptions.pkce = true; - } + options = util.clone(options) || {}; // build params using defaults + options var oauthParams = getDefaultOAuthParams(sdk); - util.extend(oauthParams, oauthOptions); + util.extend(oauthParams, options); - if (oauthParams.pkce !== true) { - return Q.resolve(oauthParams); + if (oauthParams.pkce === false) { + return Promise.resolve(oauthParams); } // PKCE flow if (!sdk.features.isPKCESupported()) { - return Q.reject(new AuthSdkError('This browser doesn\'t support PKCE')); + return Promise.reject(new AuthSdkError('This browser doesn\'t support PKCE')); } // set default code challenge method, if none provided @@ -613,25 +640,15 @@ function prepareOauthParams(sdk, oauthOptions) { }); } -function getWithRedirect(sdk, oauthOptions, options) { - oauthOptions = util.clone(oauthOptions) || {}; +function getWithRedirect(sdk, options) { + if (arguments.length > 2) { + return Promise.reject(new AuthSdkError('As of version 3.0, "getWithRedirect" takes only a single set of options')); + } + options = util.clone(options) || {}; - return prepareOauthParams(sdk, oauthOptions) + return prepareOauthParams(sdk, options) .then(function(oauthParams) { - - // Dynamically set the responseMode unless the user has provided one - // Server-side flow requires query. Client-side apps usually prefer fragment. - if (!oauthOptions.responseMode) { - if (oauthParams.responseType.includes('code') && !oauthParams.pkce) { - // server-side flows using authorization_code - oauthParams.responseMode = 'query'; - } else { - // Client-side flow can use fragment or query. This can be configured on the SDK instance. - oauthParams.responseMode = sdk.options.responseMode || 'fragment'; - } - } - - var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); + var urls = oauthUtil.getOAuthUrls(sdk, options); var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams); // Set session cookie to store the oauthParams @@ -644,16 +661,19 @@ function getWithRedirect(sdk, oauthOptions, options) { urls: urls, ignoreSignature: oauthParams.ignoreSignature }), null, { + secure: sdk.options.secureCookies, sameSite: 'none' }); // Set nonce cookie for servers to validate nonce in id_token cookies.set(constants.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce, null, { + secure: sdk.options.secureCookies, sameSite: 'none' }); // Set state cookie for servers to validate state cookies.set(constants.REDIRECT_STATE_COOKIE_NAME, oauthParams.state, null, { + secure: sdk.options.secureCookies, sameSite: 'none' }); @@ -663,7 +683,7 @@ function getWithRedirect(sdk, oauthOptions, options) { function renewToken(sdk, token) { if (!oauthUtil.isToken(token)) { - return Q.reject(new AuthSdkError('Renew must be passed a token with ' + + return Promise.reject(new AuthSdkError('Renew must be passed a token with ' + 'an array of scopes and an accessToken or idToken')); } @@ -678,11 +698,15 @@ function renewToken(sdk, token) { return sdk.token.getWithoutPrompt({ responseType: responseType, - scopes: token.scopes - }, { + scopes: token.scopes, authorizeUrl: token.authorizeUrl, userinfoUrl: token.userinfoUrl, issuer: token.issuer + }) + .then(function(res) { + // Multiple tokens may have come back. Return only the token which was requested. + var tokens = res.tokens; + return token.idToken ? tokens.idToken : tokens.accessToken; }); } @@ -714,8 +738,11 @@ function parseFromUrl(sdk, options) { options = { url: options }; } + // https://openid.net/specs/openid-connect-core-1_0.html#Authentication + var defaultResponseMode = sdk.options.pkce ? 'query' : 'fragment'; + var url = options.url; - var responseMode = options.responseMode || sdk.options.responseMode || 'fragment'; + var responseMode = options.responseMode || sdk.options.responseMode || defaultResponseMode; var nativeLoc = sdk.token.parseFromUrl._getLocation(); var paramStr; @@ -726,12 +753,12 @@ function parseFromUrl(sdk, options) { } if (!paramStr) { - return Q.reject(new AuthSdkError('Unable to parse a token from the url')); + return Promise.reject(new AuthSdkError('Unable to parse a token from the url')); } var oauthParamsCookie = cookies.get(constants.REDIRECT_OAUTH_PARAMS_COOKIE_NAME); if (!oauthParamsCookie) { - return Q.reject(new AuthSdkError('Unable to retrieve OAuth redirect params cookie')); + return Promise.reject(new AuthSdkError('Unable to retrieve OAuth redirect params cookie')); } try { @@ -740,11 +767,11 @@ function parseFromUrl(sdk, options) { delete oauthParams.urls; cookies.delete(constants.REDIRECT_OAUTH_PARAMS_COOKIE_NAME); } catch(e) { - return Q.reject(new AuthSdkError('Unable to parse the ' + + return Promise.reject(new AuthSdkError('Unable to parse the ' + constants.REDIRECT_OAUTH_PARAMS_COOKIE_NAME + ' cookie: ' + e.message)); } - return Q.resolve(oauthUtil.urlParamsToObject(paramStr)) + return Promise.resolve(oauthUtil.urlParamsToObject(paramStr)) .then(function(res) { if (!url) { // Clean hash or search from the url @@ -754,17 +781,38 @@ function parseFromUrl(sdk, options) { }); } -function getUserInfo(sdk, accessTokenObject) { +async function getUserInfo(sdk, accessTokenObject, idTokenObject) { + // If token objects were not passed, attempt to read from the TokenManager + if (!accessTokenObject) { + accessTokenObject = await sdk.tokenManager.get('accessToken'); + } + if (!idTokenObject) { + idTokenObject = await sdk.tokenManager.get('idToken'); + } + if (!accessTokenObject || (!oauthUtil.isToken(accessTokenObject) && !accessTokenObject.accessToken && !accessTokenObject.userinfoUrl)) { - return Q.reject(new AuthSdkError('getUserInfo requires an access token object')); + return Promise.reject(new AuthSdkError('getUserInfo requires an access token object')); + } + + if (!idTokenObject || + (!oauthUtil.isToken(idTokenObject) && !idTokenObject.idToken)) { + return Promise.reject(new AuthSdkError('getUserInfo requires an ID token object')); } + return http.httpRequest(sdk, { url: accessTokenObject.userinfoUrl, method: 'GET', accessToken: accessTokenObject.accessToken }) - .fail(function(err) { + .then(userInfo => { + // Only return the userinfo response if subjects match to mitigate token substitution attacks + if (userInfo.sub === idTokenObject.claims.sub) { + return userInfo; + } + return Promise.reject(new AuthSdkError('getUserInfo request was rejected due to token mismatch')); + }) + .catch(function(err) { if (err.xhr && (err.xhr.status === 401 || err.xhr.status === 403)) { var authenticateHeader; if (err.xhr.headers && util.isFunction(err.xhr.headers.get) && err.xhr.headers.get('WWW-Authenticate')) { diff --git a/packages/okta-auth-js/lib/tx.js b/packages/okta-auth-js/lib/tx.js index 71b3050d4..5a6c60645 100644 --- a/packages/okta-auth-js/lib/tx.js +++ b/packages/okta-auth-js/lib/tx.js @@ -14,7 +14,6 @@ /* eslint-disable complexity, max-statements */ var http = require('./http'); var util = require('./util'); -var Q = require('q'); var AuthSdkError = require('./errors/AuthSdkError'); var AuthPollStopError = require('./errors/AuthPollStopError'); var constants = require('./constants'); @@ -37,7 +36,7 @@ function getStateToken(res) { function transactionStatus(sdk, args) { args = addStateToken(sdk, args); - return http.post(sdk, sdk.options.url + '/api/v1/authn', args); + return http.post(sdk, sdk.getIssuerOrigin() + '/api/v1/authn', args); } function resumeTransaction(sdk, args) { @@ -48,7 +47,7 @@ function resumeTransaction(sdk, args) { stateToken: stateToken }; } else { - return Q.reject(new AuthSdkError('No transaction to resume')); + return Promise.reject(new AuthSdkError('No transaction to resume')); } } return sdk.tx.status(args) @@ -65,7 +64,7 @@ function introspect (sdk, args) { stateToken: stateToken }; } else { - return Q.reject(new AuthSdkError('No transaction to evaluate')); + return Promise.reject(new AuthSdkError('No transaction to evaluate')); } } return transactionStep(sdk, args) @@ -77,7 +76,7 @@ function introspect (sdk, args) { function transactionStep(sdk, args) { args = addStateToken(sdk, args); // v1 pipeline introspect API - return http.post(sdk, sdk.options.url + '/api/v1/authn/introspect', args); + return http.post(sdk, sdk.getIssuerOrigin() + '/api/v1/authn/introspect', args); } function transactionExists(sdk) { @@ -121,7 +120,7 @@ function getPollFn(sdk, res, ref) { opts.autoPush = !!autoPush(); } catch (e) { - return Q.reject(new AuthSdkError('AutoPush resulted in an error.')); + return Promise.reject(new AuthSdkError('AutoPush resulted in an error.')); } } else if (autoPush !== undefined && autoPush !== null) { @@ -132,7 +131,7 @@ function getPollFn(sdk, res, ref) { opts.rememberDevice = !!rememberDevice(); } catch (e) { - return Q.reject(new AuthSdkError('RememberDevice resulted in an error.')); + return Promise.reject(new AuthSdkError('RememberDevice resulted in an error.')); } } else if (rememberDevice !== undefined && rememberDevice !== null) { @@ -151,7 +150,7 @@ function getPollFn(sdk, res, ref) { var recursivePoll = function () { // If the poll was manually stopped during the delay if (!ref.isPolling) { - return Q.reject(new AuthPollStopError()); + return Promise.reject(new AuthPollStopError()); } return pollFn() .then(function (pollRes) { @@ -171,8 +170,7 @@ function getPollFn(sdk, res, ref) { } // Continue poll - return Q.delay(delay) - .then(recursivePoll); + return util.delay(delay).then(recursivePoll); } else { // Any non-waiting result, even if polling was stopped @@ -181,21 +179,21 @@ function getPollFn(sdk, res, ref) { return new AuthTransaction(sdk, pollRes); } }) - .fail(function(err) { + .catch(function(err) { // Exponential backoff, up to 16 seconds if (err.xhr && (err.xhr.status === 0 || err.xhr.status === 429) && retryCount <= 4) { var delayLength = Math.pow(2, retryCount) * 1000; retryCount++; - return Q.delay(delayLength) + return util.delay(delayLength) .then(recursivePoll); } throw err; }); }; return recursivePoll() - .fail(function(err) { + .catch(function(err) { ref.isPolling = false; throw err; }); @@ -252,7 +250,7 @@ function link2fn(sdk, res, obj, link, ref) { params.autoPush = !!autoPush(); } catch (e) { - return Q.reject(new AuthSdkError('AutoPush resulted in an error.')); + return Promise.reject(new AuthSdkError('AutoPush resulted in an error.')); } } else if (autoPush !== null) { @@ -268,7 +266,7 @@ function link2fn(sdk, res, obj, link, ref) { params.rememberDevice = !!rememberDevice(); } catch (e) { - return Q.reject(new AuthSdkError('RememberDevice resulted in an error.')); + return Promise.reject(new AuthSdkError('RememberDevice resulted in an error.')); } } else if (rememberDevice !== null) { @@ -371,7 +369,7 @@ function AuthTransaction(sdk, res) { // when OKTA-75434 is resolved if (res.status === 'RECOVERY_CHALLENGE' && !res._links) { this.cancel = function() { - return new Q(new AuthTransaction(sdk)); + return Promise.resolve(new AuthTransaction(sdk)); }; } } diff --git a/packages/okta-auth-js/lib/util.js b/packages/okta-auth-js/lib/util.js index 4bd7475a9..e7a631f2c 100644 --- a/packages/okta-auth-js/lib/util.js +++ b/packages/okta-auth-js/lib/util.js @@ -9,7 +9,8 @@ * * See the License for the specific language governing permissions and limitations under the License. */ -/* eslint-env es6 */ +/* global window, document, btoa, atob, Uint8Array */ + var util = module.exports; // converts a string to base64 (url/filename safe variant) @@ -87,10 +88,6 @@ util.isNumber = function(obj) { return Object.prototype.toString.call(obj) === '[object Number]'; }; -util.isArray = function(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; -}; - util.isoToUTCString = function(str) { var parts = str.match(/\d+/g), isoTime = Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]), @@ -272,3 +269,9 @@ util.isIE11OrLess = function() { util.isFunction = function(fn) { return !!fn && {}.toString.call(fn) === '[object Function]'; }; + +util.delay = function(ms) { + return new Promise(function(resolve) { + setTimeout(resolve, ms); + }); +}; \ No newline at end of file diff --git a/packages/okta-auth-js/lib/vendor/polyfills.js b/packages/okta-auth-js/lib/vendor/polyfills.js deleted file mode 100644 index 0622a428c..000000000 --- a/packages/okta-auth-js/lib/vendor/polyfills.js +++ /dev/null @@ -1,86 +0,0 @@ -/*! - * Copyright (c) 2015-2016, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - */ - -require('Base64'); - -// Production steps of ECMA-262, Edition 5, 15.4.4.14 -// Reference: http://es5.github.io/#x15.4.4.14 -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function(searchElement, fromIndex) { - - var k; - - // 1. Let o be the result of calling ToObject passing - // the this value as the argument. - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - - // 2. Let lenValue be the result of calling the Get - // internal method of o with the argument "length". - // 3. Let len be ToUint32(lenValue). - var len = o.length >>> 0; - - // 4. If len is 0, return -1. - if (len === 0) { - return -1; - } - - // 5. If argument fromIndex was passed let n be - // ToInteger(fromIndex); else let n be 0. - var n = +fromIndex || 0; - - if (Math.abs(n) === Infinity) { - n = 0; - } - - // 6. If n >= len, return -1. - if (n >= len) { - return -1; - } - - // 7. If n >= 0, then Let k be n. - // 8. Else, n<0, Let k be len - abs(n). - // If k is less than 0, then let k be 0. - k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); - - // 9. Repeat, while k < len - while (k < len) { - // a. Let Pk be ToString(k). - // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the - // HasProperty internal method of o with argument Pk. - // This step can be combined with c - // c. If kPresent is true, then - // i. Let elementK be the result of calling the Get - // internal method of o with the argument ToString(k). - // ii. Let same be the result of applying the - // Strict Equality Comparison Algorithm to - // searchElement and elementK. - // iii. If same is true, return k. - if (k in o && o[k] === searchElement) { - return k; - } - k++; - } - return -1; - }; -} - -if (!Array.isArray) { - Array.isArray = function(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; - }; -} diff --git a/packages/okta-auth-js/none/index.js b/packages/okta-auth-js/none/index.js deleted file mode 100644 index 8a2133fbb..000000000 --- a/packages/okta-auth-js/none/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - * - */ -var storageUtil = require('../lib/browser/browserStorage'); - -module.exports = require('../lib/browser/browser')(storageUtil); diff --git a/packages/okta-auth-js/package.json b/packages/okta-auth-js/package.json index ea88014dd..839a928ea 100644 --- a/packages/okta-auth-js/package.json +++ b/packages/okta-auth-js/package.json @@ -1,7 +1,7 @@ { "name": "@okta/okta-auth-js", "description": "The Okta Auth SDK", - "version": "2.13.1", + "version": "3.0.0", "homepage": "https://github.com/okta/okta-auth-js", "license": "Apache-2.0", "main": "lib/server/serverIndex.js", @@ -31,25 +31,37 @@ "auth", "login" ], + "browserslist": [ + "> 0.1%", + "not safari < 7.1", + "not ie < 11", + "not IE_Mob 11" + ], + "engines": { + "node": ">=10.3" + }, "dependencies": { "Base64": "0.3.0", "cross-fetch": "^3.0.0", "js-cookie": "2.2.0", "node-cache": "^4.2.0", - "q": "1.4.1", - "reqwest": "2.0.5", "tiny-emitter": "1.1.0", "xhr2": "0.1.3" }, "devDependencies": { + "@babel/cli": "^7.8.0", + "@babel/core": "^7.8.0", + "@babel/plugin-transform-runtime": "^7.8.3", + "@babel/preset-env": "^7.8.2", "babel-jest": "^24.9.0", + "babel-loader": "^8.0.6", "eslint": "5.6.1", + "eslint-plugin-compat": "^3.3.0", "eslint-plugin-jasmine": "^2.10.1", "istanbul-instrumenter-loader": "^3.0.1", "jasmine-ajax": "^4.0.0", "jest": "^24.9.0", "jest-junit": "^9.0.0", - "jquery": "3.3.1", "json-loader": "0.5.4", "karma": "^4.1.0", "karma-chrome-launcher": "^2.2.0", @@ -60,7 +72,6 @@ "karma-webpack": "^3.0.5", "lodash": "4.17.11", "promise.allsettled": "^1.0.1", - "promise.prototype.finally": "^3.1.1", "webpack": "^3.0.0" }, "jest-junit": { diff --git a/packages/okta-auth-js/reqwest/index.js b/packages/okta-auth-js/reqwest/index.js deleted file mode 100644 index 1e61a745a..000000000 --- a/packages/okta-auth-js/reqwest/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - * - */ - -var reqwestRequest = require('./reqwestRequest'); -var storageUtil = require('../lib/browser/browserStorage'); - -module.exports = require('../lib/browser/browser')(storageUtil, reqwestRequest); diff --git a/packages/okta-auth-js/reqwest/reqwestRequest.js b/packages/okta-auth-js/reqwest/reqwestRequest.js deleted file mode 100644 index 8d90a6316..000000000 --- a/packages/okta-auth-js/reqwest/reqwestRequest.js +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - */ - -var reqwest = require('reqwest'); - -function reqwestRequest(method, url, args) { - // TODO: support content-type and withCredentials - var r = reqwest({ - url: url, - method: method, - headers: args.headers, - data: JSON.stringify(args.data), - withCredentials: args.withCredentials - }) - .then(function() { - return r.request; - }); - return r; -} - -module.exports = reqwestRequest; diff --git a/packages/okta-auth-js/test/.eslintrc.json b/packages/okta-auth-js/test/.eslintrc.json new file mode 100644 index 000000000..aee689681 --- /dev/null +++ b/packages/okta-auth-js/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "compat/compat": 0 + } +} diff --git a/packages/okta-auth-js/test/karma/spec/crypto.js b/packages/okta-auth-js/test/karma/spec/crypto.js index 90b7de26f..1feae4bad 100644 --- a/packages/okta-auth-js/test/karma/spec/crypto.js +++ b/packages/okta-auth-js/test/karma/spec/crypto.js @@ -4,6 +4,15 @@ var sdkCrypto = require('../../../lib/crypto'); var sdkUtil = require('../../../lib/util'); describe('crypto', function() { + describe('getOidcHash', () => { + it('produces the correct value', () => { + // Values are taken from OAuth2JWTTokenServiceImplUnitTest in okta-core + return sdkCrypto.getOidcHash('dNZX1hEZ9wBCzNL40Upu646bdzQA') + .then(atHash => { + expect(atHash).toBe('wfgvmE9VxjAudsl9lc6TqA'); + }); + }); + }); describe('verifyToken', function() { diff --git a/packages/okta-auth-js/test/karma/spec/loginFlow.js b/packages/okta-auth-js/test/karma/spec/loginFlow.js index 9d8ee2a43..7bb08ea6d 100644 --- a/packages/okta-auth-js/test/karma/spec/loginFlow.js +++ b/packages/okta-auth-js/test/karma/spec/loginFlow.js @@ -92,7 +92,10 @@ describe('Complete login flow', function() { } function mockWellKnown() { - sdk.options.storageUtil.getHttpCache().clearStorage(); + sdk.options.storageUtil.getHttpCache({ + secure: sdk.options.secureCookies, + sameSite: 'none' + }).clearStorage(); var wellKnown = { 'jwks_uri': JWKS_URI, @@ -120,7 +123,9 @@ describe('Complete login flow', function() { it('implicit login flow', function() { // First hit /authorize - return bootstrap({}) + return bootstrap({ + pkce: false + }) .then(function(app) { return app.loginRedirect({ nonce: NONCE @@ -139,7 +144,6 @@ describe('Complete login flow', function() { expect(url.searchParams.get('client_id')).toBe(CLIENT_ID); expect(url.searchParams.get('redirect_uri')).toBe(REDIRECT_URI); expect(url.searchParams.get('response_type')).toBe('id_token token'); - expect(url.searchParams.get('response_mode')).toBe('fragment'); expect(url.searchParams.get('scope')).toBe('openid email'); expect(url.searchParams.get('state')).toBeTruthy(); expect(url.searchParams.get('nonce')).toBe(NONCE); @@ -150,7 +154,7 @@ describe('Complete login flow', function() { mockWellKnown(); const state = url.searchParams.get('state'); const pathname = `${CALLBACK_PATH}#access_token=${ACCESS_TOKEN}&id_token=${ID_TOKEN}&state=${state}&nonce=${NONCE}&expires_in=1000`; - return bootstrap({}, pathname) + return bootstrap({ pkce: false }, pathname) .then(function() { expect(getLocation).toHaveBeenCalled(); @@ -190,7 +194,6 @@ describe('Complete login flow', function() { expect(url.searchParams.get('client_id')).toBe(CLIENT_ID); expect(url.searchParams.get('redirect_uri')).toBe(REDIRECT_URI); expect(url.searchParams.get('response_type')).toBe('code'); - expect(url.searchParams.get('response_mode')).toBe('fragment'); expect(url.searchParams.get('scope')).toBe('openid email'); expect(url.searchParams.get('state')).toBeTruthy(); expect(url.searchParams.get('nonce')).toBeTruthy(); @@ -202,7 +205,7 @@ describe('Complete login flow', function() { // Now we handle the redirect & hit /token const state = url.searchParams.get('state'); - const pathname = `${CALLBACK_PATH}#code=${AUTHORIZATION_CODE}&state=${state}`; + const pathname = `${CALLBACK_PATH}?code=${AUTHORIZATION_CODE}&state=${state}`; const tokenResponse = { 'access_token': ACCESS_TOKEN, 'id_token': ID_TOKEN, diff --git a/packages/okta-auth-js/test/karma/spec/renewToken.js b/packages/okta-auth-js/test/karma/spec/renewToken.js index dc9332bf6..0bf2d58c1 100644 --- a/packages/okta-auth-js/test/karma/spec/renewToken.js +++ b/packages/okta-auth-js/test/karma/spec/renewToken.js @@ -55,7 +55,10 @@ describe('Renew token', function() { } function mockWellKnown() { - sdk.options.storageUtil.getHttpCache().clearStorage(); + sdk.options.storageUtil.getHttpCache({ + secure: sdk.options.secureCookies, + sameSite: 'none' + }).clearStorage(); var wellKnown = { 'jwks_uri': JWKS_URI, @@ -164,7 +167,7 @@ describe('Renew token', function() { it('implicit flow', function() { return bootstrap({ - + pkce: false }) .then(() => { sdk.tokenManager.add('accessToken', ACCCESS_TOKEN_PARSED); diff --git a/packages/okta-auth-js/test/spec/browser.js b/packages/okta-auth-js/test/spec/browser.js index 8ad4d8c41..eb44e7906 100644 --- a/packages/okta-auth-js/test/spec/browser.js +++ b/packages/okta-auth-js/test/spec/browser.js @@ -1,3 +1,4 @@ +/* global window */ jest.mock('cross-fetch'); var Emitter = require('tiny-emitter'); @@ -5,9 +6,24 @@ var OktaAuth = require('../../lib/browser/browserIndex'); var AuthApiError = require('../../lib/errors/AuthApiError'); describe('Browser', function() { - var auth; + let auth; + let issuer; + let originalLocation; + + afterEach(() => { + global.window.location = originalLocation; + }); + beforeEach(function() { - auth = new OktaAuth({ url: 'http://my-okta-domain' }); + originalLocation = global.window.location; + delete global.window.location; + global.window.location = { + protocol: 'https:', + hostname: 'somesite.local' + }; + + issuer = 'http://my-okta-domain'; + auth = new OktaAuth({ issuer, pkce: false }); }); it('is a valid constructor', function() { @@ -18,7 +34,7 @@ describe('Browser', function() { it('Listens to error events from TokenManager', function() { jest.spyOn(Emitter.prototype, 'on'); jest.spyOn(OktaAuth.prototype, '_onTokenManagerError'); - var auth = new OktaAuth({ url: 'http://localhost/fake' }); + var auth = new OktaAuth({ issuer: 'http://localhost/fake', pkce: false }); expect(Emitter.prototype.on).toHaveBeenCalledWith('error', auth._onTokenManagerError, auth); var emitter = Emitter.prototype.on.mock.instances[0]; var error = { errorCode: 'anything'}; @@ -29,7 +45,7 @@ describe('Browser', function() { it('error with errorCode "login_required" and accessToken: true will call option "onSessionExpired" function', function() { var onSessionExpired = jest.fn(); jest.spyOn(Emitter.prototype, 'on'); - new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired }); + new OktaAuth({ issuer: 'http://localhost/fake', pkce: false, onSessionExpired: onSessionExpired }); var emitter = Emitter.prototype.on.mock.instances[0]; expect(onSessionExpired).not.toHaveBeenCalled(); var error = { errorCode: 'login_required', accessToken: true }; @@ -40,7 +56,7 @@ describe('Browser', function() { it('error with errorCode "login_required" (not accessToken) does not call option "onSessionExpired" function', function() { var onSessionExpired = jest.fn(); jest.spyOn(Emitter.prototype, 'on'); - new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired }); + new OktaAuth({ issuer: 'http://localhost/fake', pkce: false, onSessionExpired: onSessionExpired }); var emitter = Emitter.prototype.on.mock.instances[0]; expect(onSessionExpired).not.toHaveBeenCalled(); var error = { errorCode: 'login_required' }; @@ -51,7 +67,7 @@ describe('Browser', function() { it('error with unknown errorCode does not call option "onSessionExpired" function', function() { var onSessionExpired = jest.fn(); jest.spyOn(Emitter.prototype, 'on'); - new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired }); + new OktaAuth({ issuer: 'http://localhost/fake', pkce: false, onSessionExpired: onSessionExpired }); var emitter = Emitter.prototype.on.mock.instances[0]; expect(onSessionExpired).not.toHaveBeenCalled(); var error = { errorCode: 'unknown', accessToken: true }; @@ -61,29 +77,52 @@ describe('Browser', function() { }); describe('options', function() { + describe('secureCookies', () => { - describe('PKCE', function() { + it('is true by default', () => { + expect(auth.options.secureCookies).toBe(true); + }); - it('is false by default', function() { - expect(auth.options.pkce).toBe(false); + it('throws if running on HTTP', () => { + global.window.location.protocol = 'http:'; + function fn() { + auth = new OktaAuth({ issuer: 'http://my-okta-domain' }); + } + expect(fn).toThrowError( + 'The current page is not being served with the HTTPS protocol.\n' + + 'For security reasons, we strongly recommend using HTTPS.\n' + + 'If you cannot use HTTPS, set the "secureCookies" option to false.' + ); }); - it('can be set by arg', function() { - spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); - auth = new OktaAuth({ pkce: true, url: 'http://my-okta-domain' }); - expect(auth.options.pkce).toBe(true); + it('does not throw if running on HTTP and secureCookies = false', () => { + global.window.location.protocol = 'http:'; + function fn() { + auth = new OktaAuth({ secureCookies: false, issuer: 'http://my-okta-domain', pkce: false }); + } + expect(fn).not.toThrow(); }); - it('accepts alias "grantType"', function() { + }); + + describe('PKCE', function() { + + it('is true by default', function() { spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); - auth = new OktaAuth({ grantType: "authorization_code", url: 'http://my-okta-domain' }); + auth = new OktaAuth({ issuer }); expect(auth.options.pkce).toBe(true); }); - it('throws if PKCE is not supported', function() { + it('can be set to "false" by arg', function() { + auth = new OktaAuth({ pkce: false, issuer: 'http://my-okta-domain' }); + expect(auth.options.pkce).toBe(false); + }); + + it('throws if PKCE is not supported (HTTP)', function() { spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(false); + global.window.location.protocol = 'http:'; function fn() { - auth = new OktaAuth({ pkce: true, url: 'http://my-okta-domain' }); + auth = new OktaAuth({ pkce: true, secureCookies: false, issuer: 'http://my-okta-domain' }); } expect(fn).toThrowError( 'PKCE requires a modern browser with encryption support running in a secure context.\n' + @@ -91,331 +130,356 @@ describe('Browser', function() { '"TextEncoder" is not defined. You may need a polyfill/shim for this browser.' ); }); + + it('throws if PKCE is not supported (HTTPS, no TextEncoder)', function() { + spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(false); + expect(global.window.TextEncoder).toBe(undefined); + function fn() { + auth = new OktaAuth({ pkce: true, issuer: 'http://my-okta-domain' }); + } + expect(fn).toThrowError( + 'PKCE requires a modern browser with encryption support running in a secure context.\n' + + '"TextEncoder" is not defined. You may need a polyfill/shim for this browser.' + ); + }); + + it('throws if PKCE is not supported (HTTPS, with TextEncoder)', function() { + spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(false); + global.window.TextEncoder = {}; + function fn() { + auth = new OktaAuth({ pkce: true, issuer: 'http://my-okta-domain' }); + } + expect(fn).toThrowError( + 'PKCE requires a modern browser with encryption support running in a secure context.' + ); + }); }) }); - describe('signOut', function() { - beforeEach(function() { - global.window.location.assign = jest.fn(); - }); - it('Default options: clear TokenManager, close session, no redirect', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - return auth.signOut() + describe('revokeAccessToken', function() { + it('will read from TokenManager and call token.revoke', function() { + var accessToken = { accessToken: 'fake' }; + spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(accessToken)); + spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); + return auth.revokeAccessToken() .then(function() { - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - expect(window.location.assign).not.toHaveBeenCalled(); + expect(auth.tokenManager.get).toHaveBeenCalledWith('accessToken'); + expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); }); }); - describe('revokeAccessToken', function() { - it('will call token.revoke', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(accessToken)); - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - return auth.signOut({ revokeAccessToken: true }) - .then(function() { - expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - }); + it('will throw if token.revoke rejects with unknown error', function() { + var accessToken = { accessToken: 'fake' }; + spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(accessToken)); + var testError = new Error('test error'); + spyOn(auth.token, 'revoke').and.callFake(function() { + return Promise.reject(testError); }); - it('will throw if token.revoke rejects with unknown error', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(accessToken)); - var testError = new Error('test error'); - spyOn(auth.token, 'revoke').and.callFake(function() { - return Promise.reject(testError); - }); - return auth.signOut({ revokeAccessToken: true }) - .catch(function(e) { - expect(e).toBe(testError); - }); - }); - it('will not throw if token.revoke rejects with AuthApiError', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(accessToken)); - var testError = new AuthApiError({}); - spyOn(auth.token, 'revoke').and.callFake(function() { - return Promise.reject(testError); + return auth.revokeAccessToken() + .catch(function(e) { + expect(e).toBe(testError); }); - return auth.signOut({ revokeAccessToken: true }) + }); + it('can pass an access token object to bypass TokenManager', function() { + var accessToken = { accessToken: 'fake' }; + spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); + spyOn(auth.tokenManager, 'get'); + return auth.revokeAccessToken(accessToken) .then(function() { + expect(auth.tokenManager.get).not.toHaveBeenCalled(); expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); + }); + }); + it('if accessToken cannot be located, will resolve without error', function() { + spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); + spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve()) + return auth.revokeAccessToken() + .then(() => { + expect(auth.tokenManager.get).toHaveBeenCalled(); + expect(auth.token.revoke).not.toHaveBeenCalled(); + }); + }); + }); + + describe('closeSession', function() { + it('Default options: clears TokenManager, closes session', function() { + spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); + spyOn(auth.tokenManager, 'clear'); + return auth.closeSession() + .then(function() { expect(auth.tokenManager.clear).toHaveBeenCalled(); expect(auth.session.close).toHaveBeenCalled(); }); + }); + it('catches and absorbs "AuthApiError" errors with errorCode E0000007 (RESOURCE_NOT_FOUND_EXCEPTION)', function() { + var testError = new AuthApiError({ errorCode: 'E0000007' }); + spyOn(auth.session, 'close').and.callFake(function() { + return Promise.reject(testError); }); - it('by default, will read access token from TokenManager using key "token"', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(accessToken)); - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - return auth.signOut({ revokeAccessToken: true }) - .then(function() { - expect(auth.tokenManager.get).toHaveBeenCalledWith('token'); - expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - }); - }); - it('can pass an access token object', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'get'); - return auth.signOut({ revokeAccessToken: true, accessToken: accessToken }) - .then(function() { - expect(auth.tokenManager.get).not.toHaveBeenCalled(); - expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - }); + return auth.closeSession() + .then(function() { + expect(auth.session.close).toHaveBeenCalled(); }); - it('if accessToken=false, will not revoke or read from TokenManager', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'get'); - return auth.signOut({ revokeAccessToken: true, accessToken: false }) - .then(function() { - expect(auth.tokenManager.get).not.toHaveBeenCalled(); - expect(auth.token.revoke).not.toHaveBeenCalled(); - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - }); + }); + it('will throw unknown errors', function() { + var testError = new Error('test error'); + spyOn(auth.session, 'close').and.callFake(function() { + return Promise.reject(testError); }); - it('if accessToken cannot be located, will not attempt revoke', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve()) - return auth.signOut({ revokeAccessToken: true }) - .then(function() { - expect(auth.tokenManager.get).toHaveBeenCalled(); - expect(auth.token.revoke).not.toHaveBeenCalled(); - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - }); + return auth.closeSession() + .catch(function(e) { + expect(e).toBe(testError); }); - it('can be combined with "postLogoutRedirectUri"', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - var idToken = { idToken: 'fake' }; + }); + }); - spyOn(auth.tokenManager, 'get'); - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - return auth.signOut({ revokeAccessToken: true, accessToken: accessToken, postLogoutRedirectUri: 'http://someother', idToken: idToken }) - .then(function() { - expect(auth.tokenManager.get).not.toHaveBeenCalled(); - expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).not.toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://my-okta-domain/oauth2/v1/logout?id_token_hint=fake&post_logout_redirect_uri=http%3A%2F%2Fsomeother'); - }); + describe('signOut', function() { + let origin; + let encodedOrigin; + + beforeEach(function() { + origin = 'https://somesite.local'; + encodedOrigin = encodeURIComponent(origin); + Object.assign(global.window.location, { + origin, + assign: jest.fn(), + reload: jest.fn() }); + }); - it('can be combined with "postLogoutRedirectUri" (no id token)', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); + describe('with idToken and accessToken', () => { + let idToken; + let accessToken; + + function initSpies() { + spyOn(auth.tokenManager, 'get').and.callFake(key => { + if (key === 'idToken') { + return idToken; + } else if (key === 'token') { + return accessToken; + } else { + throw new Error(`Unknown token key: ${key}`); + } + }); spyOn(auth.tokenManager, 'clear'); - var accessToken = { accessToken: 'fake' }; - var idToken = false; + spyOn(auth, 'revokeAccessToken').and.returnValue(Promise.resolve()); + spyOn(auth, 'closeSession').and.returnValue(Promise.resolve()); + } - spyOn(auth.tokenManager, 'get'); - spyOn(auth.token, 'revoke').and.returnValue(Promise.resolve()); - return auth.signOut({ revokeAccessToken: true, accessToken: accessToken, postLogoutRedirectUri: 'http://someother', idToken: idToken }) + beforeEach(() => { + accessToken = { accessToken: 'fake' }; + idToken = { idToken: 'fake' }; + initSpies(); + }); + + it('Default options: will revokeAccessToken and use window.location.href for postLogoutRedirectUri', function() { + return auth.signOut() .then(function() { - expect(auth.tokenManager.get).not.toHaveBeenCalled(); - expect(auth.token.revoke).toHaveBeenCalledWith(accessToken); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(1, 'idToken'); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(2, 'token'); + expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://someother'); + expect(auth.closeSession).not.toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); - }); - describe('closeSession', function() { - it('will throw unknown errors', function() { - var testError = new Error('test error'); - spyOn(auth.session, 'close').and.callFake(function() { - return Promise.reject(testError); - }); - return auth.signOut() - .catch(function(e) { - expect(e).toBe(testError); - }); - }); - it('catches and absorbs "AuthApiError" errors', function() { - var testError = new AuthApiError({}); - spyOn(auth.session, 'close').and.callFake(function() { - return Promise.reject(testError); - }); - return auth.signOut() - .then(function() { - expect(auth.session.close).toHaveBeenCalled(); - }); - }); - }) - describe('postLogoutRedirectUri', function() { - it('can be set by config', function() { + + it('supports custom authorization server', function() { + issuer = 'http://my-okta-domain/oauth2/custom-as'; auth = new OktaAuth({ - url: 'http://my-okta-domain', - postLogoutRedirectUri: 'http://someother' + pkce: false, + issuer }); - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); + initSpies(); return auth.signOut() .then(function() { - expect(window.location.assign).toHaveBeenCalledWith('http://someother'); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); - - it('supports custom authorization server', function() { - auth = new OktaAuth({ - url: 'http://my-okta-domain', - issuer: 'http://my-okta-domain/oauth2/custom-as', - }); - var idToken = { idToken: 'fake' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(idToken)); - return auth.signOut({ postLogoutRedirectUri: 'http://someother' }) + + it('if idToken is passed, will skip token manager read', function() { + var customToken = { idToken: 'fake-custom' }; + return auth.signOut({ idToken: customToken }) .then(function() { - expect(window.location.assign).toHaveBeenCalledWith('http://my-okta-domain/oauth2/custom-as/v1/logout?id_token_hint=fake&post_logout_redirect_uri=http%3A%2F%2Fsomeother'); + expect(auth.tokenManager.get).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(1, 'token'); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${customToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); - it('will catch exceptions from session.close and perform redirect', function() { - auth = new OktaAuth({ - url: 'http://my-okta-domain', - postLogoutRedirectUri: 'http://someother' - }); - var testError = new Error('test error'); - spyOn(auth.session, 'close').and.callFake(function() { - return Promise.reject(testError); - }); - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(null)); - return auth.signOut() + it('if idToken=false will skip token manager read and call closeSession', function() { + return auth.signOut({ idToken: false }) .then(function() { - expect(auth.session.close).toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://someother'); + expect(auth.tokenManager.get).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(1, 'token'); + expect(auth.closeSession).toHaveBeenCalled(); + expect(window.location.reload).toHaveBeenCalled(); }); }); - - it('by default, will try to get idToken from TokenManager', function() { - var idToken = { idToken: 'fake' }; - spyOn(auth.tokenManager, 'clear').and.callFake(function() { - // Catch condition where clear() is called before the idToken is read - idToken = false; - }); - - spyOn(auth.tokenManager, 'get').and.callFake(function() { - return idToken; - }) - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - return auth.signOut({ postLogoutRedirectUri: 'http://someother' }) - .then(function() { - expect(auth.tokenManager.get).toHaveBeenCalledWith('idToken'); - expect(auth.session.close).not.toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://my-okta-domain/oauth2/v1/logout?id_token_hint=fake&post_logout_redirect_uri=http%3A%2F%2Fsomeother'); + describe('postLogoutRedirectUri', function() { + it('can be set by config', function() { + const postLogoutRedirectUri = 'http://someother'; + const encodedUri = encodeURIComponent(postLogoutRedirectUri); + auth = new OktaAuth({ + pkce: false, + issuer, + postLogoutRedirectUri }); + initSpies(); + return auth.signOut() + .then(function() { + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedUri}`); + }); + }); + it('can be passed as an option', function() { + const postLogoutRedirectUri = 'http://someother'; + const encodedUri = encodeURIComponent(postLogoutRedirectUri); + return auth.signOut({ postLogoutRedirectUri }) + .then(function() { + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedUri}`); + }); + }); }); - - it('if idToken is passed, will skip token manager read and do location redirect', function() { - var idToken = { idToken: 'fake' }; - var customToken = { idToken: 'fake-custom' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(idToken)); - return auth.signOut({ idToken: customToken, postLogoutRedirectUri: 'http://someother' }) + + it('Can pass a "state" option', function() { + const state = 'foo=bar&yo=me'; + const encodedState = encodeURIComponent(state); + return auth.signOut({ state }) .then(function() { - expect(auth.tokenManager.get).not.toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://my-okta-domain/oauth2/v1/logout?id_token_hint=fake-custom&post_logout_redirect_uri=http%3A%2F%2Fsomeother'); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}&state=${encodedState}`); }); }); - - it('if idToken=false will skip token manager read and use session.close before redirecting', function() { - var idToken = { idToken: 'fake' }; - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(idToken)); - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - return auth.signOut({ idToken: false, postLogoutRedirectUri: 'http://someother' }) + + it('Can pass a "revokeAccessToken=false" to skip accessToken logic', function() { + return auth.signOut({ revokeAccessToken: false }) .then(function() { - expect(auth.tokenManager.get).not.toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://someother'); + expect(auth.tokenManager.get).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(1, 'idToken'); + expect(auth.revokeAccessToken).not.toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); - - - it('redirect: (no idToken) - will close session and redirect to uri', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - return auth.signOut({ postLogoutRedirectUri: 'http://someother' }) + + it('Can pass a "accessToken=false" to skip accessToken logic', function() { + return auth.signOut({ accessToken: false }) .then(function() { - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://someother'); + expect(auth.tokenManager.get).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(1, 'idToken'); + expect(auth.revokeAccessToken).not.toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); - - it('idToken: without a redirect option, it will not use logout redirect', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); + }); + + describe('without idToken', () => { + let accessToken; + + beforeEach(() => { + accessToken = { accessToken: 'fake' }; + spyOn(auth.tokenManager, 'get').and.callFake(key => { + if (key === 'idToken') { + return; + } else if (key === 'token') { + return accessToken; + } else { + throw new Error(`Unknown token key: ${key}`); + } + }); spyOn(auth.tokenManager, 'clear'); - return auth.signOut({ idToken: { idToken: 'fake' } }) + spyOn(auth, 'revokeAccessToken').and.returnValue(Promise.resolve()); + }); + + it('Default options: will revokeAccessToken and fallback to closeSession and window.location.reload()', function() { + spyOn(auth, 'closeSession').and.returnValue(Promise.resolve()); + return auth.signOut() .then(function() { + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(1, 'idToken'); + expect(auth.tokenManager.get).toHaveBeenNthCalledWith(2, 'token'); + expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).toHaveBeenCalled(); - expect(window.location.assign).not.toHaveBeenCalled(); + expect(auth.closeSession).toHaveBeenCalled(); + expect(window.location.reload).toHaveBeenCalled(); }); }); - - it('idToken + postLogoutRedirectUri: will use logout redirect with "post_logout_redirect_uri"', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - return auth.signOut({ idToken: { idToken: 'fake' }, postLogoutRedirectUri: 'http://someother' }) + + it('Default options: will throw exceptions from closeSession and not call window.location.reload', function() { + const testError = new Error('test error'); + spyOn(auth, 'closeSession').and.callFake(function() { + return Promise.reject(testError); + }); + return auth.signOut() .then(function() { - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).not.toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://my-okta-domain/oauth2/v1/logout?id_token_hint=fake&post_logout_redirect_uri=http%3A%2F%2Fsomeother'); + expect(false).toBe(true); + }) + .catch(function(e) { + expect(e).toBe(testError); + expect(auth.closeSession).toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); }); }); - - it('idToken + postLogoutRedirectUri + state: logout redirect url includes "state" and "post_logout_redirect_uri"', function() { - spyOn(auth.session, 'close').and.returnValue(Promise.resolve()); - spyOn(auth.tokenManager, 'clear'); - return auth.signOut({ idToken: { idToken: 'fake' }, postLogoutRedirectUri: 'http://someother', state: 'foo=bar&yo=me' }) + + it('with postLogoutRedirectUri: will call window.location.assign', function() { + const postLogoutRedirectUri = 'http://someother'; + spyOn(auth, 'closeSession').and.returnValue(Promise.resolve()); + return auth.signOut({ postLogoutRedirectUri }) .then(function() { - expect(auth.tokenManager.clear).toHaveBeenCalled(); - expect(auth.session.close).not.toHaveBeenCalled(); - expect(window.location.assign).toHaveBeenCalledWith('http://my-okta-domain/oauth2/v1/logout?id_token_hint=fake&post_logout_redirect_uri=http%3A%2F%2Fsomeother&state=foo%3Dbar%26yo%3Dme'); + expect(window.location.assign).toHaveBeenCalledWith(postLogoutRedirectUri); }); }); - }); - describe('XHR logout', function() { - it('will not catch exceptions from session.close', function() { - auth = new OktaAuth({ - url: 'http://my-okta-domain', - }); - var testError = new Error('test error'); - spyOn(auth.session, 'close').and.callFake(function() { + it('with postLogoutRedirectUri: will throw exceptions from closeSession and not call window.location.assign', function() { + const postLogoutRedirectUri = 'http://someother'; + const testError = new Error('test error'); + spyOn(auth, 'closeSession').and.callFake(function() { return Promise.reject(testError); }); - spyOn(auth.tokenManager, 'get').and.returnValue(Promise.resolve(null)); - return auth.signOut() + return auth.signOut({ postLogoutRedirectUri }) + .then(function() { + expect(false).toBe(true); + }) .catch(function(e) { expect(e).toBe(testError); - }) - .then(function() { - expect(auth.session.close).toHaveBeenCalled(); + expect(auth.closeSession).toHaveBeenCalled(); + expect(window.location.assign).not.toHaveBeenCalled(); }); }); }); + + describe('without accessToken', () => { + let idToken; + beforeEach(() => { + idToken = { idToken: 'fake' }; + spyOn(auth.tokenManager, 'get').and.callFake(key => { + if (key === 'idToken') { + return idToken; + } else if (key === 'token') { + return; + } else { + throw new Error(`Unknown token key: ${key}`); + } + }); + spyOn(auth.tokenManager, 'clear'); + spyOn(auth, 'revokeAccessToken').and.returnValue(Promise.resolve()); + }); + + it('Default options: will not revoke accessToken', () => { + return auth.signOut() + .then(function() { + expect(auth.revokeAccessToken).not.toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); + }); + }); + + it('Can pass an accessToken', () => { + const accessToken = { accessToken: 'custom-fake' }; + return auth.signOut({ accessToken }) + .then(function() { + expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); + }); + }); + }); + }); }); \ No newline at end of file diff --git a/packages/okta-auth-js/test/spec/browserStorage.js b/packages/okta-auth-js/test/spec/browserStorage.js new file mode 100644 index 000000000..c6d9c0051 --- /dev/null +++ b/packages/okta-auth-js/test/spec/browserStorage.js @@ -0,0 +1,192 @@ +jest.mock('../../lib/storageBuilder'); + +var browserStorage = require('../../lib/browser/browserStorage'); +var storageBuilder = require('../../lib/storageBuilder'); + +describe('browserStorage', () => { + let originalLocalStorage; + let originalSessionStorage; + + beforeEach(() => { + originalLocalStorage = global.window.localStorage; + originalSessionStorage = global.window.sessionStorage; + }); + + afterEach(() => { + global.window.localStorage = originalLocalStorage; + global.window.sessionStorage = originalSessionStorage; + }); + + it('can return localStorage', () => { + expect(global.window.localStorage).toBeDefined(); + expect(browserStorage.getLocalStorage()).toBe(global.localStorage); + }); + + it('can return sessionStorage', () => { + expect(global.window.sessionStorage).toBeDefined(); + expect(browserStorage.getSessionStorage()).toBe(global.sessionStorage); + }); + + describe('browserHasLocalStorage', () => { + it('returns true if storage exists and passes test', () => { + expect(browserStorage.browserHasLocalStorage()).toBe(true); + }); + it('returns false if localStorage does not exist', () => { + delete global.window.localStorage; + expect(browserStorage.browserHasLocalStorage()).toBe(false); + }); + it('returns false if testStorage() returns false', () => { + jest.spyOn(browserStorage, 'testStorage').mockReturnValue(false); + expect(browserStorage.browserHasLocalStorage()).toBe(false); + }); + }); + + describe('browserHasSessionStorage', () => { + it('returns true if storage exists and passes test', () => { + expect(browserStorage.browserHasSessionStorage()).toBe(true); + }); + it('returns false if sessionStorage does not exist', () => { + delete global.window.sessionStorage; + expect(browserStorage.browserHasSessionStorage()).toBe(false); + }); + it('returns false if testStorage() returns false', () => { + jest.spyOn(browserStorage, 'testStorage').mockReturnValue(false); + expect(browserStorage.browserHasSessionStorage()).toBe(false); + }); + }); + + describe('testStorage', () => { + it('returns true if no exception is thrown', () => { + const fakeStorage = { + removeItem: jest.fn(), + setItem: jest.fn() + } + expect(browserStorage.testStorage(fakeStorage)).toBe(true); + expect(fakeStorage.setItem).toHaveBeenCalledWith('okta-test-storage', 'okta-test-storage'); + expect(fakeStorage.removeItem).toHaveBeenCalledWith('okta-test-storage'); + }); + it('returns false if an exception is thrown on removeItem', () => { + const fakeStorage = { + removeItem: jest.fn().mockImplementation(() => { + throw new Error('removeItem fails'); + }), + setItem: jest.fn() + } + expect(browserStorage.testStorage(fakeStorage)).toBe(false); + }); + it('returns false if an exception is thrown on setItem', () => { + const fakeStorage = { + removeItem: jest.fn(), + setItem: jest.fn().mockImplementation(() => { + throw new Error('setItem fails'); + }), + } + expect(browserStorage.testStorage(fakeStorage)).toBe(false); + }); + }); + + describe('getPKCEStorage', () => { + it('Uses localStorage by default', () => { + browserStorage.getPKCEStorage(); + expect(storageBuilder).toHaveBeenCalledWith(global.window.localStorage, 'okta-pkce-storage'); + }); + it('Uses sessionStorage if localStorage is not available', () => { + delete global.window.localStorage; + browserStorage.getPKCEStorage(); + expect(storageBuilder).toHaveBeenCalledWith(global.window.sessionStorage, 'okta-pkce-storage'); + }); + it('Uses cookie storage if localStorage and sessionStorage are not available', () => { + delete global.window.localStorage; + delete global.window.sessionStorage; + const fakeStorage = { fakeStorage: true }; + jest.spyOn(browserStorage, 'getCookieStorage').mockReturnValue(fakeStorage); + const opts = { fakeOptions: true }; + browserStorage.getPKCEStorage(opts); + expect(storageBuilder).toHaveBeenCalledWith(fakeStorage, 'okta-pkce-storage'); + expect(browserStorage.getCookieStorage).toHaveBeenCalledWith(opts); + }); + }); + + describe('getHttpCache', () => { + it('Uses localStorage by default', () => { + browserStorage.getHttpCache(); + expect(storageBuilder).toHaveBeenCalledWith(global.window.localStorage, 'okta-cache-storage'); + }); + it('Uses sessionStorage if localStorage is not available', () => { + delete global.window.localStorage; + browserStorage.getHttpCache(); + expect(storageBuilder).toHaveBeenCalledWith(global.window.sessionStorage, 'okta-cache-storage'); + }); + it('Uses cookie storage if localStorage and sessionStorage are not available', () => { + delete global.window.localStorage; + delete global.window.sessionStorage; + const fakeStorage = { fakeStorage: true }; + jest.spyOn(browserStorage, 'getCookieStorage').mockReturnValue(fakeStorage); + const opts = { fakeOptions: true }; + browserStorage.getHttpCache(opts); + expect(storageBuilder).toHaveBeenCalledWith(fakeStorage, 'okta-cache-storage'); + expect(browserStorage.getCookieStorage).toHaveBeenCalledWith(opts); + }); + }); + + describe('getCookieStorage', () => { + it('requires an options object', () => { + const fn = function() { + browserStorage.getCookieStorage(); + }; + expect(fn).toThrowError('Cannot read property \'secure\' of undefined'); + }); + + it('requires a "secure" option', () => { + const fn = function() { + browserStorage.getCookieStorage({}); + }; + expect(fn).toThrowError('getCookieStorage: "secure" and "sameSite" options must be provided'); + }); + + it('requires a "sameSite" option', () => { + const fn = function() { + browserStorage.getCookieStorage({ secure: true }); + }; + expect(fn).toThrowError('getCookieStorage: "secure" and "sameSite" options must be provided'); + }); + + it('Can pass false for "secure" and "sameSite"', () => { + const fn = function() { + browserStorage.getCookieStorage({ secure: false, sameSite: false }); + }; + expect(fn).not.toThrow(); + }); + + it('getItem: will call storage.get', () => { + const retVal = { fakeCookie: true }; + jest.spyOn(browserStorage.storage, 'get').mockReturnValue(retVal); + const storage = browserStorage.getCookieStorage({ secure: true, sameSite: 'strict' }); + const key = 'fake-key'; + expect(storage.getItem(key)).toBe(retVal); + expect(browserStorage.storage.get).toHaveBeenCalledWith(key); + }); + + it('setItem: will call storage.set, passing secure and sameSite options', () => { + jest.spyOn(browserStorage.storage, 'set').mockReturnValue(null); + const storage = browserStorage.getCookieStorage({ secure: 'fakey', sameSite: 'strictly fakey' }); + const key = 'fake-key'; + const val = { fakeValue: true }; + storage.setItem(key, val); + expect(browserStorage.storage.set).toHaveBeenCalledWith(key, val, '2200-01-01T00:00:00.000Z', { + secure: 'fakey', + sameSite: 'strictly fakey' + }); + }) + }); + + describe('getInMemoryStorage', () => { + it('can set and retrieve a value from memory', () => { + const storage = browserStorage.getInMemoryStorage(); + const key = 'fake-key'; + const val = { fakeValue: true }; + storage.setItem(key, val); + expect(storage.getItem(key)).toBe(val); + }) + }) +}); \ No newline at end of file diff --git a/packages/okta-auth-js/test/spec/cookies.js b/packages/okta-auth-js/test/spec/cookies.js index 42cbb6fd0..3795f74e7 100644 --- a/packages/okta-auth-js/test/spec/cookies.js +++ b/packages/okta-auth-js/test/spec/cookies.js @@ -9,45 +9,59 @@ describe('cookie', function () { }); describe('set', function () { + it('Throws if "secure" option is not set', () => { + function testFunc() { Cookies.set('foo', 'bar', null, { sameSite: 'strict' }); } + expect(testFunc).toThrow('storage.set: "secure" and "sameSite" options must be provided') + }); it('proxies JsCookie.set', function () { - Cookies.set('foo', 'bar'); + Cookies.set('foo', 'bar', null, { secure: true, sameSite: 'strict' }); expect(JsCookie.set).toHaveBeenCalledWith('foo', 'bar', { - path: '/' + path: '/', + secure: true, + sameSite: 'strict' }); }); it('proxies JsCookie.set with an expiry time', function () { - Cookies.set('foo', 'bar', '2200-01-01T00:00:00.000Z'); + Cookies.set('foo', 'bar', '2200-01-01T00:00:00.000Z', { secure: true, sameSite: 'strict' }); expect(JsCookie.set).toHaveBeenCalledWith('foo', 'bar', { path: '/', - expires: new Date('2200-01-01T00:00:00.000Z') + expires: new Date('2200-01-01T00:00:00.000Z'), + secure: true, + sameSite: 'strict' }); }); it('proxies JsCookie.set with an invalid expiry time', function () { - Cookies.set('foo', 'bar', 'not a valid date'); + Cookies.set('foo', 'bar', 'not a valid date', { secure: true, sameSite: 'strict' }); expect(JsCookie.set).toHaveBeenCalledWith('foo', 'bar', { - path: '/' + path: '/', + secure: true, + sameSite: 'strict' }); }); it('proxies JsCookie.set with "secure" setting', function () { Cookies.set('foo', 'bar', null, { - secure: true + secure: false, + sameSite: 'strict' }); expect(JsCookie.set).toHaveBeenCalledWith('foo', 'bar', { path: '/', - secure: true + secure: false, + sameSite: 'strict' }); }); it('proxies JsCookie.set with "sameSite" setting', function () { Cookies.set('foo', 'bar', null, { - sameSite: 'none' + secure: true, + sameSite: 'lax' }); expect(JsCookie.set).toHaveBeenCalledWith('foo', 'bar', { path: '/', - sameSite: 'none' + secure: true, + sameSite: 'lax' }); }); }); diff --git a/packages/okta-auth-js/test/spec/errors.js b/packages/okta-auth-js/test/spec/errors.js index c14186ee0..fd23d541e 100644 --- a/packages/okta-auth-js/test/spec/errors.js +++ b/packages/okta-auth-js/test/spec/errors.js @@ -83,7 +83,7 @@ describe('General Errors', function () { expect(err.errorSummary).toEqual('No arguments passed to constructor. Required usage: new OktaAuth(args)'); }); - it('throw an error if no url and no issuer are passed to the constructor', function () { + it('throw an error if no issuer is passed to the constructor', function () { var err; try { new OktaAuth({}); // eslint-disable-line no-new @@ -91,11 +91,11 @@ describe('General Errors', function () { err = e; } expect(err.name).toEqual('AuthSdkError'); - expect(err.errorSummary).toEqual('No url passed to constructor. ' + - 'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com"})'); + expect(err.errorSummary).toEqual('No issuer passed to constructor. ' + + 'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})'); }); - it('throw an error if issuer is not a url and url is omitted when passed to the constructor', function () { + it('throw an error if issuer is not a url', function () { var err; try { new OktaAuth({issuer: 'default'}); // eslint-disable-line no-new @@ -103,19 +103,19 @@ describe('General Errors', function () { err = e; } expect(err.name).toEqual('AuthSdkError'); - expect(err.errorSummary).toEqual('No url passed to constructor. ' + - 'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com"})'); + expect(err.errorSummary).toEqual('Issuer must be a valid URL. ' + + 'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})'); }); it('throw an error if url contains "-admin" when passed to the constructor', function () { var err; try { - new OktaAuth({url: 'https://dev-12345-admin.oktapreview.com'}); // eslint-disable-line no-new + new OktaAuth({issuer: 'https://dev-12345-admin.oktapreview.com'}); // eslint-disable-line no-new } catch (e) { err = e; } expect(err.name).toEqual('AuthSdkError'); - expect(err.errorSummary).toEqual('URL passed to constructor contains "-admin" in subdomain. ' + - 'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com})'); + expect(err.errorSummary).toEqual('Issuer URL passed to constructor contains "-admin" in subdomain. ' + + 'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com})'); }); }); diff --git a/packages/okta-auth-js/test/spec/features.js b/packages/okta-auth-js/test/spec/features.js index 8fe4e46c1..8e15e91b4 100644 --- a/packages/okta-auth-js/test/spec/features.js +++ b/packages/okta-auth-js/test/spec/features.js @@ -1,3 +1,4 @@ +/* global window, document */ var OktaAuth = require('OktaAuth'); describe('features', function() { @@ -11,6 +12,7 @@ describe('features', function() { it('on prototype', function() { var auth = new OktaAuth({ + pkce: false, issuer: 'https://fakeo' }); @@ -109,7 +111,7 @@ describe('features', function() { spyOn(OktaAuth.features, 'isHTTPS').and.returnValue(true); try { new OktaAuth({ - url: 'https://dev-12345.oktapreview.com', + issuer: 'https://dev-12345.oktapreview.com', pkce: true, }); } catch (e) { @@ -126,7 +128,7 @@ describe('features', function() { spyOn(OktaAuth.features, 'isHTTPS').and.returnValue(false); try { new OktaAuth({ - url: 'https://dev-12345.oktapreview.com', + issuer: 'https://dev-12345.oktapreview.com', pkce: true, }); } catch (e) { @@ -146,7 +148,7 @@ describe('features', function() { window.TextEncoder = undefined; try { new OktaAuth({ - url: 'https://dev-12345.oktapreview.com', + issuer: 'https://dev-12345.oktapreview.com', pkce: true, }); } catch (e) { @@ -166,7 +168,7 @@ describe('features', function() { window.TextEncoder = undefined; try { new OktaAuth({ - url: 'https://dev-12345.oktapreview.com', + issuer: 'https://dev-12345.oktapreview.com', pkce: true, }); } catch (e) { diff --git a/packages/okta-auth-js/test/spec/fetch-request.js b/packages/okta-auth-js/test/spec/fetch-request.js index c5130122f..ae34e65c6 100644 --- a/packages/okta-auth-js/test/spec/fetch-request.js +++ b/packages/okta-auth-js/test/spec/fetch-request.js @@ -1,87 +1,178 @@ +/* global Map */ describe('fetchRequest', function () { - var Q = require('q'); - var mockFetchResult; - var mockFetchObj = { + let fetchSpy; + + let requestHeaders; + let requestMethod; + let requestUrl; + let response; + let responseHeaders; + let responseJSON; + let responseText; + + const mockFetchObj = { fetch: function mockFetchFunc() { - return Q.resolve(mockFetchResult); + return Promise.resolve(response); } } jest.setMock('cross-fetch', function() { return mockFetchObj.fetch.apply(null, arguments); }); - - var fetchRequest = require('../../fetch/fetchRequest'); + const fetchRequest = require('../../lib/fetch/fetchRequest'); beforeEach(function() { - /* global Map */ - mockFetchResult = { - headers: new Map(), + fetchSpy = jest.spyOn(mockFetchObj, 'fetch'); + responseHeaders = new Map(); + responseHeaders.set('Content-Type', 'application/json'); + responseJSON = { isFakeResponse: true }; + responseText = JSON.stringify(responseJSON); + response = { + headers: responseHeaders, + status: 200, + ok: true, json: function() { - return Q.resolve(); + return Promise.resolve(responseJSON); }, text: function() { - return Q.resolve(); + return Promise.resolve(responseText); } + }; + + requestHeaders = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', } + requestMethod = 'GET'; + requestUrl = 'http://fakey.local'; }); - it('JSON encodes request body if request Content-Type is application/json', function() { - var spy = jest.spyOn(mockFetchObj, 'fetch'); - var method = 'GET'; - var url = 'http://fakey.local'; - var headers = { - 'Content-Type': 'application/json' - }; - var obj = { - foo: 'bar' - }; - var jsonObj = JSON.stringify(obj); + describe('request', () => { + it('JSON encodes request body if request header Content-Type is application/json', function() { + const requestJSON = { + foo: 'bar' + }; + return fetchRequest(requestMethod, requestUrl, { + headers: requestHeaders, + data: requestJSON + }) + .then(() => { + expect(fetchSpy).toHaveBeenCalledWith(requestUrl, { + method: requestMethod, + headers: requestHeaders, + body: JSON.stringify(requestJSON), + credentials: 'include' + }); + }); + }); - fetchRequest(method, url, { - headers: headers, - data: obj + it('Leaves request body unchanged if request header Content-Type is NOT application/json', function() { + requestHeaders = { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }; + const requestText = 'string=1&fake=2'; + return fetchRequest(requestMethod, requestUrl, { + headers: requestHeaders, + data: requestText + }) + .then(() => { + expect(fetchSpy).toHaveBeenCalledWith(requestUrl, { + method: requestMethod, + headers: requestHeaders, + body: requestText, + credentials: 'include' + }); + }); }); - expect(spy).toHaveBeenCalledWith(url, { - method: method, - headers: headers, - body: jsonObj, - credentials: 'include' + + it('Can omit credentials', function() { + return fetchRequest(requestMethod, requestUrl, { + withCredentials: false + }) + .then(() => { + expect(fetchSpy).toHaveBeenCalledWith(requestUrl, { + method: requestMethod, + credentials: 'omit' + }); + }); }); }); - it('Leaves request body unchanged if request Content-Type is NOT application/json', function() { - var spy = jest.spyOn(mockFetchObj, 'fetch'); - var method = 'GET'; - var url = 'http://fakey.local'; - var obj = { - foo: 'bar' - }; + describe('response', () => { - fetchRequest(method, url, { - data: obj + it('Returns JSON if response header Content-Type is application/json', function() { + return fetchRequest(requestMethod, requestUrl, {}) + .then(res => { + expect(res).toEqual({ + status: response.status, + responseJSON, + responseText, + responseType: 'json' + }); + }); }); - expect(spy).toHaveBeenCalledWith(url, { - method: method, - body: obj, - credentials: 'include' + it('Returns text if response header Content-Type is NOT application/json', function() { + responseHeaders.set('Content-Type', 'application/x-www-form-urlencoded'); + return fetchRequest(requestMethod, requestUrl, {}) + .then(res => { + expect(res).toEqual({ + status: response.status, + responseText + }); + }); }); - }); - it('Can omit credentials', function() { - var spy = jest.spyOn(mockFetchObj, 'fetch'); - var method = 'GET'; - var url = 'http://fakey.local'; + it('Throws the response if response.ok is false (JSON)', () => { + response.status = 401; + response.ok = false; + return fetchRequest(requestMethod, requestUrl, {}) + .catch(err => { + expect(err).toEqual({ + status: response.status, + responseText, + responseType: 'json', + responseJSON + }); + }); + }); - fetchRequest(method, url, { - withCredentials: false + it('Throws the response if response.ok is false (text)', () => { + response.status = 401; + response.ok = false; + responseHeaders.set('Content-Type', 'application/x-www-form-urlencoded'); + return fetchRequest(requestMethod, requestUrl, {}) + .catch(err => { + expect(err).toEqual({ + status: response.status, + responseText + }); + }); }); - expect(spy).toHaveBeenCalledWith(url, { - method: method, - credentials: 'omit' + it('Throws the response if response.ok is false (invalid JSON)', () => { + var error = new Error('A fake error, ignore me'); + response.status = 401; + response.ok = false; + response.json = function() { + return Promise.reject(error); + }; + + var errorJSON = { + error: error, + errorSummary: 'Could not parse server response' + }; + + return fetchRequest(requestMethod, requestUrl, {}) + .catch(err => { + expect(err).toEqual({ + status: response.status, + responseText: JSON.stringify(errorJSON), + responseJSON: errorJSON, + responseType: 'json' + }); + }); }); }); - }); diff --git a/packages/okta-auth-js/test/spec/fingerprint.js b/packages/okta-auth-js/test/spec/fingerprint.js index caf5e95a4..359a2b243 100644 --- a/packages/okta-auth-js/test/spec/fingerprint.js +++ b/packages/okta-auth-js/test/spec/fingerprint.js @@ -1,3 +1,4 @@ +/* global window, document */ jest.mock('cross-fetch'); var OktaAuth = require('OktaAuth'); @@ -69,7 +70,8 @@ describe('fingerprint', function() { }); var authClient = options.authClient || new OktaAuth({ - url: 'http://example.okta.com' + pkce: false, + issuer: 'http://example.okta.com' }); if (typeof options.userAgent !== 'undefined') { util.mockUserAgent(authClient, options.userAgent); @@ -152,7 +154,7 @@ describe('fingerprint', function() { util.itMakesCorrectRequestResponse({ title: 'attaches fingerprint to signIn requests if sendFingerprint is true', setup: { - uri: 'http://example.okta.com', + issuer: 'http://example.okta.com', calls: [ { request: { @@ -182,7 +184,7 @@ describe('fingerprint', function() { util.itMakesCorrectRequestResponse({ title: 'does not attach fingerprint to signIn requests if sendFingerprint is false', setup: { - uri: 'http://example.okta.com', + issuer: 'http://example.okta.com', calls: [ { request: { diff --git a/packages/okta-auth-js/test/spec/general.js b/packages/okta-auth-js/test/spec/general.js index b92ed7f7d..38a16841b 100644 --- a/packages/okta-auth-js/test/spec/general.js +++ b/packages/okta-auth-js/test/spec/general.js @@ -45,7 +45,7 @@ describe('General Methods', function () { }, execute: function (test) { return test.oa.signIn({}) - .fail(function(err) { + .catch(function(err) { expect(err.errorCode).toEqual('E0000004'); expect(err.errorSummary).toEqual('Authentication failed'); expect(err.errorLink).toEqual('E0000004'); diff --git a/packages/okta-auth-js/test/spec/mfa-challenge.js b/packages/okta-auth-js/test/spec/mfa-challenge.js index cb3c15a41..ea564516f 100644 --- a/packages/okta-auth-js/test/spec/mfa-challenge.js +++ b/packages/okta-auth-js/test/spec/mfa-challenge.js @@ -1,16 +1,21 @@ jest.mock('cross-fetch'); var util = require('@okta/test.support/util'); +var sdkUtil = require('../../lib/util'); -var Q = require('q'); -function mockQDelay() { - var original = Q.delay; - jest.spyOn(Q, 'delay').mockImplementation(function() { - return original.call(this, 0); +function mockDelay() { + jest.spyOn(sdkUtil, 'delay').mockImplementation(function() { + return Promise.resolve(); }); } + + describe('MFA_CHALLENGE', function () { + beforeEach(() => { + mockDelay(); + }); + describe('trans.verify', function () { util.itMakesCorrectRequestResponse({ title: 'allows verification with passCode', @@ -1254,7 +1259,6 @@ describe('MFA_CHALLENGE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); } }); @@ -1294,7 +1298,6 @@ describe('MFA_CHALLENGE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); } }); @@ -1426,7 +1429,6 @@ describe('MFA_CHALLENGE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); }, expectations: function (test, err) { @@ -1503,7 +1505,6 @@ describe('MFA_CHALLENGE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); }, expectations: function (test, err) { diff --git a/packages/okta-auth-js/test/spec/mfa-enroll-activate.js b/packages/okta-auth-js/test/spec/mfa-enroll-activate.js index 61536da8a..3f40a11ca 100644 --- a/packages/okta-auth-js/test/spec/mfa-enroll-activate.js +++ b/packages/okta-auth-js/test/spec/mfa-enroll-activate.js @@ -1,16 +1,18 @@ jest.mock('cross-fetch'); var util = require('@okta/test.support/util'); -var Q = require('q'); +var sdkUtil = require('../../lib/util'); -function mockQDelay() { - var original = Q.delay; - jest.spyOn(Q, 'delay').mockImplementation(function() { - return original.call(this, 0); +function mockDelay() { + jest.spyOn(sdkUtil, 'delay').mockImplementation(function() { + return Promise.resolve(); }); } describe('MFA_ENROLL_ACTIVATE', function () { + beforeEach(() => { + mockDelay(); + }); describe('trans.poll', function () { util.itMakesCorrectRequestResponse({ title: 'allows polling for push', @@ -104,7 +106,6 @@ describe('MFA_ENROLL_ACTIVATE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); } }); @@ -144,7 +145,6 @@ describe('MFA_ENROLL_ACTIVATE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); } }); @@ -211,7 +211,6 @@ describe('MFA_ENROLL_ACTIVATE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); }, expectations: function (test, err) { @@ -306,7 +305,6 @@ describe('MFA_ENROLL_ACTIVATE', function () { ] }, execute: function (test) { - mockQDelay(); return test.trans.poll(0); }, expectations: function (test, err) { diff --git a/packages/okta-auth-js/test/spec/mfa-required.js b/packages/okta-auth-js/test/spec/mfa-required.js index 7ba85ac3e..fcfb97d8e 100644 --- a/packages/okta-auth-js/test/spec/mfa-required.js +++ b/packages/okta-auth-js/test/spec/mfa-required.js @@ -509,7 +509,7 @@ describe('MFA_REQUIRED', function () { passCode: 'invalidanswer', rememberDevice: true }) - .fail(function(err) { + .catch(function(err) { invalidError = err; }) .then(function() { @@ -518,7 +518,7 @@ describe('MFA_REQUIRED', function () { rememberDevice: true }); }) - .fin(function() { + .finally(function() { expect(invalidError).not.toBeUndefined(); }); } @@ -557,7 +557,7 @@ describe('MFA_REQUIRED', function () { passCode: 'invalidanswer', autoPush: true }) - .fail(function(err) { + .catch(function(err) { invalidError = err; }) .then(function() { @@ -566,7 +566,7 @@ describe('MFA_REQUIRED', function () { autoPush: true }); }) - .fin(function() { + .finally(function() { expect(invalidError).not.toBeUndefined(); }); } diff --git a/packages/okta-auth-js/test/spec/oauthUtil.js b/packages/okta-auth-js/test/spec/oauthUtil.js index 678190d79..62d445207 100644 --- a/packages/okta-auth-js/test/spec/oauthUtil.js +++ b/packages/okta-auth-js/test/spec/oauthUtil.js @@ -1,3 +1,4 @@ +/* global window, localStorage, sessionStorage */ jest.mock('cross-fetch'); var OktaAuth = require('OktaAuth'); @@ -252,7 +253,8 @@ describe('getWellKnown', function() { }), '2200-01-01T00:00:00.000Z', { - sameSite: 'none' + sameSite: 'none', + secure: false // due to localhost exemption } ); } @@ -267,7 +269,7 @@ describe('getKey', function() { }, execute: function(test) { oauthUtilHelpers.loadWellKnownAndKeysCache(); - return oauthUtil.getKey(test.oa, test.oa.options.url, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); + return oauthUtil.getKey(test.oa, null, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); }, expectations: function(test, key) { expect(key).toEqual(tokens.standardKey); @@ -289,7 +291,7 @@ describe('getKey', function() { }, execute: function(test) { oauthUtilHelpers.loadWellKnownCache(); - return oauthUtil.getKey(test.oa, test.oa.options.url, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); + return oauthUtil.getKey(test.oa, null, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); }, expectations: function(test, key) { expect(key).toEqual(tokens.standardKey); @@ -343,7 +345,7 @@ describe('getKey', function() { } })); - return oauthUtil.getKey(test.oa, test.oa.options.url, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); + return oauthUtil.getKey(test.oa, null, 'U5R8cHbGw445Qbq8zVO1PcCpXL8yG6IcovVa3laCoxM'); }, expectations: function(test, key) { expect(key).toEqual(tokens.standardKey); @@ -388,7 +390,7 @@ describe('getKey', function() { } })); - return oauthUtil.getKey(test.oa, test.oa.options.url, 'invalidKid'); + return oauthUtil.getKey(test.oa, null, 'invalidKid'); }, expectations: function(test, err) { util.assertAuthSdkError(err, 'The key id, invalidKid, was not found in the server\'s keys'); @@ -410,16 +412,13 @@ describe('getKey', function() { describe('getOAuthUrls', function() { function setupOAuthUrls(options) { var sdk = new OktaAuth(options.oktaAuthArgs || { - url: 'https://auth-js-test.okta.com' + pkce: false, + issuer: 'https://auth-js-test.okta.com' }); - var oauthParams = options.oauthParams || { - responseType: 'id_token' - }; - var result, error; try { - result = oauthUtil.getOAuthUrls(sdk, oauthParams, options.options); + result = oauthUtil.getOAuthUrls(sdk, options.options); } catch(e) { error = e; } @@ -439,6 +438,18 @@ describe('getOAuthUrls', function() { } } + it('throws if an extra options object is passed', () => { + const sdk = new OktaAuth({ + pkce: false, + issuer: 'https://auth-js-test.okta.com' + }); + + const f = function () { + oauthUtil.getOAuthUrls(sdk, {}, {}); + }; + expect(f).toThrowError('As of version 3.0, "getOAuthUrls" takes only a single set of options'); + }); + it('defaults all urls using global defaults', function() { setupOAuthUrls({ expectedResult: { @@ -454,7 +465,7 @@ describe('getOAuthUrls', function() { it('sanitizes forward slashes', function() { setupOAuthUrls({ oktaAuthArgs: { - url: 'https://auth-js-test.okta.com', + pkce: false, issuer: 'https://auth-js-test.okta.com/', logoutUrl: 'https://auth-js-test.okta.com/oauth2/v1/logout/', tokenUrl: 'https://auth-js-test.okta.com/oauth2/v1/token/', @@ -474,7 +485,7 @@ describe('getOAuthUrls', function() { it('overrides defaults with options', function() { setupOAuthUrls({ oktaAuthArgs: { - url: 'https://auth-js-test.okta.com', + pkce: false, issuer: 'https://bad.okta.com', logoutUrl: 'https://bad.okta.com/oauth2/v1/logout', revokeUrl: 'https://bad.okta.com/oauth2/v1/revoke', @@ -550,21 +561,6 @@ describe('getOAuthUrls', function() { } }); }); - it('uses authServer issuer as authServerId to generate authorizeUrl and userinfoUrl', function() { - setupOAuthUrls({ - options: { - issuer: 'aus8aus76q8iphupD0h7' - }, - expectedResult: { - issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', - logoutUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/logout', - revokeUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/revoke', - tokenUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/token', - authorizeUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/authorize', - userinfoUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo' - } - }); - }); it('allows token requested with only authorizeUrl and userinfoUrl', function() { setupOAuthUrls({ oauthParams: { @@ -584,48 +580,6 @@ describe('getOAuthUrls', function() { } }); }); - it('fails if id_token requested without issuer, with authorizeUrl', function() { - setupOAuthUrls({ - oauthParams: { - responseType: 'id_token' - }, - options: { - authorizeUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/authorize' - }, - expectedError: { - name: 'AuthSdkError', - message: 'Cannot request idToken with an authorizeUrl without an issuer' - } - }); - }); - it('fails if token requested without issuer, without userinfoUrl, with authorizeUrl', function() { - setupOAuthUrls({ - oauthParams: { - responseType: 'token' - }, - options: { - authorizeUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/authorize' - }, - expectedError: { - name: 'AuthSdkError', - message: 'Cannot request accessToken with an authorizeUrl without an issuer or userinfoUrl' - } - }); - }); - it('fails if token requested without issuer, without authorizeUrl, with userinfoUrl', function() { - setupOAuthUrls({ - oauthParams: { - responseType: 'id_token' - }, - options: { - userinfoUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo' - }, - expectedError: { - name: 'AuthSdkError', - message: 'Cannot request token with an userinfoUrl without an issuer or authorizeUrl' - } - }); - }); }); describe('loadPopup', function() { @@ -694,7 +648,8 @@ describe('validateClaims', function () { beforeEach(function() { sdk = new OktaAuth({ - url: 'https://auth-js-test.okta.com', + pkce: false, + issuer: 'https://auth-js-test.okta.com', clientId: 'foo', ignoreSignature: false }); diff --git a/packages/okta-auth-js/test/spec/pkce.js b/packages/okta-auth-js/test/spec/pkce.js index df428bd4a..a60fe2f22 100644 --- a/packages/okta-auth-js/test/spec/pkce.js +++ b/packages/okta-auth-js/test/spec/pkce.js @@ -1,8 +1,6 @@ /* global Promise */ jest.mock('cross-fetch'); -var Q = require('q'); - var util = require('@okta/test.support/util'); var factory = require('@okta/test.support/factory'); var packageJson = require('../../package.json'); @@ -19,7 +17,7 @@ describe('pkce', function() { it('throws an error if pkce is true and PKCE is not supported', function() { spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(false); - var sdk = new OktaAuth({ issuer: 'https://foo.com', }); + var sdk = new OktaAuth({ issuer: 'https://foo.com', pkce: false }); return token.prepareOauthParams(sdk, { pkce: true, }) @@ -58,7 +56,7 @@ describe('pkce', function() { it('Checks codeChallengeMethod against well-known', function() { spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); var sdk = new OktaAuth({ issuer: 'https://foo.com', pkce: true }); - spyOn(oauthUtil, 'getWellKnown').and.returnValue(Q.resolve({ + spyOn(oauthUtil, 'getWellKnown').and.returnValue(Promise.resolve({ 'code_challenge_methods_supported': [] })) return token.prepareOauthParams(sdk, {}) @@ -78,12 +76,12 @@ describe('pkce', function() { spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); var sdk = new OktaAuth({ issuer: 'https://foo.com', pkce: true }); - spyOn(oauthUtil, 'getWellKnown').and.returnValue(Q.resolve({ + spyOn(oauthUtil, 'getWellKnown').and.returnValue(Promise.resolve({ 'code_challenge_methods_supported': [codeChallengeMethod] })); spyOn(pkce, 'generateVerifier').and.returnValue(codeVerifier); spyOn(pkce, 'saveMeta'); - spyOn(pkce, 'computeChallenge').and.returnValue(Q.resolve(codeChallenge)); + spyOn(pkce, 'computeChallenge').and.returnValue(Promise.resolve(codeChallenge)); return token.prepareOauthParams(sdk, { codeChallengeMethod: codeChallengeMethod }) @@ -105,7 +103,7 @@ describe('pkce', function() { util.itMakesCorrectRequestResponse({ title: 'requests a token', setup: { - uri: ISSUER, + issuer: ISSUER, bypassCrypto: true, calls: [ { @@ -153,8 +151,9 @@ describe('pkce', function() { var oauthOptions; beforeEach(function() { + spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); authClient = new OktaAuth({ - url: 'https://auth-js-test.okta.com' + issuer: 'https://auth-js-test.okta.com' }); oauthOptions = { diff --git a/packages/okta-auth-js/test/spec/session.js b/packages/okta-auth-js/test/spec/session.js index 6e56f4c81..921474639 100644 --- a/packages/okta-auth-js/test/spec/session.js +++ b/packages/okta-auth-js/test/spec/session.js @@ -1,19 +1,24 @@ +/* global window */ var session = require('../../lib/session'); var http = require('../../lib/http'); -var Q = require('q'); describe('session', function() { var sdk; var sessionObj; + var baseUrl; beforeEach(function() { sessionObj = {}; + baseUrl = 'http://fakey'; + sdk = { + getIssuerOrigin: jest.fn().mockReturnValue(baseUrl), + options: { + issuer: baseUrl + }, session: { get: jest.fn().mockImplementation(function() { - var deferred = Q.defer(); - deferred.resolve(sessionObj); - return deferred.promise; + return Promise.resolve(sessionObj); }) } }; @@ -35,9 +40,7 @@ describe('session', function() { it('resolves to false if session.get throws', function() { sdk.session.get.mockImplementation(function() { - var deferred = Q.defer(); - deferred.reject(new Error('test error')); - return deferred.promise; + return Promise.reject(new Error('test error')); }); return session.sessionExists(sdk) .then(function(res) { @@ -58,7 +61,7 @@ describe('session', function() { describe('getSession', function() { it('Hits endpoint: /api/v1/sessions/me', function() { - jest.spyOn(http, 'get').mockReturnValue(Q()); + jest.spyOn(http, 'get').mockReturnValue(Promise.resolve()); return session.getSession(sdk) .then(function() { expect(http.get).toHaveBeenCalledWith(sdk, '/api/v1/sessions/me'); @@ -67,9 +70,7 @@ describe('session', function() { it('XHR error: returns an INACTIVE session object', function() { jest.spyOn(http, 'get').mockImplementation(function() { - var deferred = Q.defer(); - deferred.reject(new Error('test error')); - return deferred.promise; + return Promise.reject(new Error('test error')); }); return session.getSession(sdk) .then(function(res) { @@ -80,7 +81,7 @@ describe('session', function() { }); it('Adds a "refresh" method on the session object', function() { - jest.spyOn(http, 'get').mockReturnValue(Q()); + jest.spyOn(http, 'get').mockReturnValue(Promise.resolve()); return session.getSession(sdk) .then(function(res) { expect(typeof res.refresh).toBe('function'); @@ -88,7 +89,7 @@ describe('session', function() { }); it('Adds a "user" method on the session object', function() { - jest.spyOn(http, 'get').mockReturnValue(Q()); + jest.spyOn(http, 'get').mockReturnValue(Promise.resolve()); return session.getSession(sdk) .then(function(res) { expect(typeof res.user).toBe('function'); @@ -103,9 +104,7 @@ describe('session', function() { } }; jest.spyOn(http, 'get').mockImplementation(function() { - var deferred = Q.defer(); - deferred.resolve(sessionObj); - return deferred.promise; + return Promise.resolve(sessionObj); }); return session.getSession(sdk) .then(function(res) { @@ -128,9 +127,7 @@ describe('session', function() { }; jest.spyOn(http, 'post').mockReturnValue(null); jest.spyOn(http, 'get').mockImplementation(function() { - var deferred = Q.defer(); - deferred.resolve(sessionObj); - return deferred.promise; + return Promise.resolve(sessionObj); }); return session.getSession(sdk) .then(function(res) { @@ -149,9 +146,7 @@ describe('session', function() { } }; jest.spyOn(http, 'get').mockImplementation(function() { - var deferred = Q.defer(); - deferred.resolve(sessionObj); - return deferred.promise; + return Promise.resolve(sessionObj); }); return session.getSession(sdk) .then(function(res) { @@ -166,10 +161,6 @@ describe('session', function() { describe('closeSession', function() { it('makes a DELETE request to /api/v1/sessions/me', function() { jest.spyOn(http, 'httpRequest').mockReturnValue(Promise.resolve()); - var baseUrl = 'http://fakey'; - sdk.options = { - url: baseUrl - }; return session.closeSession(sdk) .then(function() { expect(http.httpRequest).toHaveBeenCalledWith(sdk, { @@ -182,9 +173,6 @@ describe('session', function() { it('will throw if http request rejects', function() { var testError = new Error('test error'); jest.spyOn(http, 'httpRequest').mockReturnValue(Promise.reject(testError)); - sdk.options = { - url: 'http://fakey' - }; return session.closeSession(sdk) // should throw .catch(function(e) { expect(e).toBe(testError); @@ -211,7 +199,6 @@ describe('session', function() { }); describe('setCookieAndRedirect', function() { - var baseUrl; var currentUrl; beforeEach(function() { currentUrl = 'http://i-am-here'; @@ -219,10 +206,6 @@ describe('session', function() { window.location = { href: currentUrl }; - baseUrl = 'http://fakey'; - sdk.options = { - url: baseUrl - } }); it('redirects to /login/sessionCookieRedirect', function() { session.setCookieAndRedirect(sdk); diff --git a/packages/okta-auth-js/test/spec/storageBuilder.js b/packages/okta-auth-js/test/spec/storageBuilder.js new file mode 100644 index 000000000..6343d0f22 --- /dev/null +++ b/packages/okta-auth-js/test/spec/storageBuilder.js @@ -0,0 +1,134 @@ +var storageBuilder = require('../../lib/storageBuilder'); + +describe('storageBuilder', () => { + + it('throws if a storagename is not provided', () => { + const fn = function() { + storageBuilder(); + }; + expect(fn).toThrowError('"storageName" is required'); + }); + + it('Returns an interface around a storage object', () => { + const storage = storageBuilder({}, 'fake'); + expect(typeof storage.getStorage).toBe('function'); + expect(typeof storage.setStorage).toBe('function'); + expect(typeof storage.clearStorage).toBe('function'); + expect(typeof storage.updateStorage).toBe('function'); + }); + + describe('getStorage', () => { + it('Calls "getItem" on the inner storage', () => { + const inner = { + getItem: jest.fn() + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + storage.getStorage(); + expect(inner.getItem).toHaveBeenCalledWith(storageName); + }); + it('JSON decodes the returned object', () => { + const obj = { fakeObject: true }; + const inner = { + getItem: jest.fn().mockReturnValue(JSON.stringify(obj)) + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + expect(storage.getStorage()).toEqual(obj); + }); + it('throws if object cannot be decoded', () => { + const inner = { + getItem: jest.fn().mockReturnValue('a string that wont decode') + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + const fn = function() { + storage.getStorage(); + } + expect(fn).toThrowError('Unable to parse storage string: fake') + }); + }); + + describe('setStorage', () => { + it('Calls "setItem" on the inner storage', () => { + const inner = { + setItem: jest.fn() + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + storage.setStorage({}); + expect(inner.setItem).toHaveBeenCalledWith(storageName, '{}'); + }); + it('JSON stringifies the object passed to inner storage', () => { + const inner = { + setItem: jest.fn() + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + const obj = { fakeObject: true, anArray: [1, 2, 3] }; + storage.setStorage(obj); + expect(inner.setItem).toHaveBeenCalledWith(storageName, JSON.stringify(obj)); + }); + it('Throws an error if setItem throws', () => { + const inner = { + setItem: jest.fn().mockImplementation(() => { + throw new Error('this error will be caught'); + }) + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + const fn = function() { + storage.setStorage({}); + } + expect(fn).toThrowError('Unable to set storage: fake'); + }); + }); + + describe('clearStorage', () => { + it('if no key is passed, it will set storage to an empty object', () => { + const inner = { + setItem: jest.fn() + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + storage.clearStorage(); + expect(inner.setItem).toHaveBeenCalledWith(storageName, '{}'); + }); + it('will remove the property with the given key', () => { + const obj = { + key1: 'a', + key2: 'b' + }; + const inner = { + setItem: jest.fn(), + getItem: jest.fn().mockReturnValue(JSON.stringify(obj)) + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + storage.clearStorage('key1'); + expect(inner.getItem).toHaveBeenCalledWith(storageName); + expect(inner.setItem).toHaveBeenCalledWith(storageName, '{"key2":"b"}'); + }); + }); + + describe('updateStorage', () => { + it('will add a property with the given key', () => { + const initialObj = { + key1: 'a' + }; + const finalObj = { + key1: 'a', + key2: 'b' + }; + const inner = { + setItem: jest.fn(), + getItem: jest.fn().mockReturnValue(JSON.stringify(initialObj)) + }; + const storageName = 'fake'; + const storage = storageBuilder(inner, storageName); + storage.updateStorage('key2', 'b'); + expect(inner.getItem).toHaveBeenCalledWith(storageName); + expect(inner.setItem).toHaveBeenCalledWith(storageName, JSON.stringify(finalObj)); + }); + }); +}); \ No newline at end of file diff --git a/packages/okta-auth-js/test/spec/token.js b/packages/okta-auth-js/test/spec/token.js index 5d92c236b..a0dd9d657 100644 --- a/packages/okta-auth-js/test/spec/token.js +++ b/packages/okta-auth-js/test/spec/token.js @@ -1,9 +1,9 @@ +/* global window, document, btoa */ jest.mock('cross-fetch'); var allSettled = require('promise.allsettled'); allSettled.shim(); // will be a no-op if not needed var _ = require('lodash'); -var Q = require('q'); var OktaAuth = require('OktaAuth'); var tokens = require('@okta/test.support/tokens'); var util = require('@okta/test.support/util'); @@ -13,12 +13,14 @@ var packageJson = require('../../package.json'); var sdkUtil = require('../../lib/oauthUtil'); var pkce = require('../../lib/pkce'); var http = require('../../lib/http'); +var sdkCrypto = require('../../lib/crypto'); function setupSync(options) { - options = Object.assign({ issuer: 'http://example.okta.com' }, options); + options = Object.assign({ issuer: 'http://example.okta.com', pkce: false }, options); return new OktaAuth(options); } + describe('token.revoke', function() { it('throws if token is not passed', function() { var oa = setupSync(); @@ -110,6 +112,7 @@ describe('token.getWithoutPrompt', function() { fn({ data: { 'id_token': tokens.standardIdToken, + 'access_token': tokens.standardAccessToken, state: states[index], }, origin: origin || 'https://auth-js-test.okta.com' @@ -118,6 +121,7 @@ describe('token.getWithoutPrompt', function() { beforeEach(function() { var oktaAuthArgs = { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -205,17 +209,46 @@ describe('token.getWithoutPrompt', function() { }); }); + it('If extra options are passed, promise will reject', function() { + return oauthUtil.setupFrame({ + willFail: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com', + }, + getWithoutPromptArgs: [{ + /* expected options */ + }, { + /* extra options */ + }] + }) + .then(function() { + expect(true).toEqual(false); + }) + .catch(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'As of version 3.0, "getWithoutPrompt" takes only a single set of options', + errorCode: 'INTERNAL', + errorSummary: 'As of version 3.0, "getWithoutPrompt" takes only a single set of options', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + }); + }); + it('If authorizeUrl does not match configured issuer, promise will reject', function() { return oauthUtil.setupFrame({ willFail: true, oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' }, getWithoutPromptArgs: [{ - sessionToken: 'testSessionToken' - }, { + sessionToken: 'testSessionToken', authorizeUrl: 'https://bogus', }], postMessageSrc: { @@ -225,7 +258,7 @@ describe('token.getWithoutPrompt', function() { .then(function() { expect(true).toEqual(false); }) - .fail(function(err) { + .catch(function(err) { util.expectErrorToEqual(err, { name: 'AuthSdkError', message: 'The request does not match client configuration', @@ -238,9 +271,10 @@ describe('token.getWithoutPrompt', function() { }); }); - it('returns id_token using sessionToken', function() { + it('returns tokens using sessionToken', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -253,7 +287,7 @@ describe('token.getWithoutPrompt', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'state': oauthUtil.mockedState, 'nonce': oauthUtil.mockedNonce, @@ -265,9 +299,10 @@ describe('token.getWithoutPrompt', function() { }); }); - it('returns id_token using sessionToken with issuer', function() { + it('returns tokens using sessionToken with issuer', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -280,7 +315,7 @@ describe('token.getWithoutPrompt', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'state': oauthUtil.mockedState, 'nonce': oauthUtil.mockedNonce, @@ -290,20 +325,26 @@ describe('token.getWithoutPrompt', function() { } }, postMessageResp: { + 'access_token': tokens.authServerAccessToken, 'id_token': tokens.authServerIdToken, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerIdTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed + } + } }); }); - it('returns id_token using sessionToken with issuer as id', function() { + it('returns tokens using sessionToken with issuer as id', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { - url: 'https://auth-js-test.okta.com', + issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', + pkce: false, clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect', - issuer: 'aus8aus76q8iphupD0h7' }, getWithoutPromptArgs: { sessionToken: 'testSessionToken' @@ -313,7 +354,7 @@ describe('token.getWithoutPrompt', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'state': oauthUtil.mockedState, 'nonce': oauthUtil.mockedNonce, @@ -323,23 +364,29 @@ describe('token.getWithoutPrompt', function() { } }, postMessageResp: { + 'access_token': tokens.authServerAccessToken, 'id_token': tokens.authServerIdToken, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerIdTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed + } + } }); }); it('allows passing issuer through getWithoutPrompt, which takes precedence', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/ORIGINAL_AUTH_SERVER_ID', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' }, getWithoutPromptArgs: [{ - sessionToken: 'testSessionToken' - }, { + sessionToken: 'testSessionToken', issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' }], postMessageSrc: { @@ -347,7 +394,7 @@ describe('token.getWithoutPrompt', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'state': oauthUtil.mockedState, 'nonce': oauthUtil.mockedNonce, @@ -357,50 +404,23 @@ describe('token.getWithoutPrompt', function() { } }, postMessageResp: { + 'access_token': tokens.authServerAccessToken, 'id_token': tokens.authServerIdToken, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerIdTokenParsed - }); - }); - - it('allows passing issuer as an id through getWithoutPrompt, which takes precedence', function() { - return oauthUtil.setupFrame({ - oktaAuthArgs: { - issuer: 'https://auth-js-test.okta.com/oauth2/ORIGINAL_AUTH_SERVER_ID', - clientId: 'NPSfOkH5eZrTy8PMDlvx', - redirectUri: 'https://example.com/redirect' - }, - getWithoutPromptArgs: [{ - sessionToken: 'testSessionToken' - }, { - issuer: 'aus8aus76q8iphupD0h7' - }], - postMessageSrc: { - baseUri: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/authorize', - queryParams: { - 'client_id': 'NPSfOkH5eZrTy8PMDlvx', - 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', - 'response_mode': 'okta_post_message', - 'state': oauthUtil.mockedState, - 'nonce': oauthUtil.mockedNonce, - 'scope': 'openid email', - 'prompt': 'none', - 'sessionToken': 'testSessionToken' + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed } - }, - postMessageResp: { - 'id_token': tokens.authServerIdToken, - 'state': oauthUtil.mockedState - }, - expectedResp: tokens.authServerIdTokenParsed + } }); }); it('returns id_token overriding all possible oauth params', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -437,16 +457,23 @@ describe('token.getWithoutPrompt', function() { 'state': 'bbbbbb' }, expectedResp: { - idToken: tokens.modifiedIdToken, - claims: tokens.modifiedIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'custom'] + state: 'bbbbbb', + tokens: { + idToken: { + idToken: tokens.modifiedIdToken, + claims: tokens.modifiedIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'custom'] + } + } } }); }); it('allows multiple iframes simultaneously', function() { - jest.useFakeTimers(); + var iframes; + var firstPrompt; + var secondPrompt; return oauthUtil.setupSimultaneousPostMessage() .then(function(context) { // mock frame creation @@ -462,25 +489,29 @@ describe('token.getWithoutPrompt', function() { }); // ensure that no iframes are open - var iframes = document.getElementsByTagName('IFRAME'); + iframes = document.getElementsByTagName('IFRAME'); expect(iframes.length).toBe(0); // getWithoutPrompt, but don't resolve - var firstPrompt = context.client.token.getWithoutPrompt({ + firstPrompt = context.client.token.getWithoutPrompt({ + responseType: 'id_token', sessionToken: 'testSessionToken', state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce }); // getWithoutPrompt, but don't resolve - var secondPrompt = context.client.token.getWithoutPrompt({ + secondPrompt = context.client.token.getWithoutPrompt({ + responseType: 'id_token', sessionToken: 'testSessionToken2', state: oauthUtil.mockedState2, nonce: oauthUtil.mockedNonce2 }); - - jest.runAllTicks(); // resolve promises - + return waitFor(function() { + return iframes.length === 2 ? context : false; + }); + }) + .then(function(context) { // assert that two iframes are open expect(iframes.length).toBe(2); @@ -488,10 +519,10 @@ describe('token.getWithoutPrompt', function() { context.emitter.emit('trigger', oauthUtil.mockedState); context.emitter.emit('trigger', oauthUtil.mockedState2); - return Q.all([firstPrompt, secondPrompt]) - .spread(function(firstToken, secondToken) { - expect(firstToken).toEqual(tokens.standardIdTokenParsed); - expect(secondToken).toEqual(tokens.standardIdToken2Parsed); + return Promise.all([firstPrompt, secondPrompt]) + .then(function(values) { + expect(values[0].tokens.idToken).toEqual(tokens.standardIdTokenParsed); + expect(values[1].tokens.idToken).toEqual(tokens.standardIdToken2Parsed); // make sure both iframes were destroyed expect(iframes.length).toBe(0); @@ -505,6 +536,7 @@ describe('token.getWithoutPrompt', function() { it('returns access_token using sessionToken', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -534,13 +566,19 @@ describe('token.getWithoutPrompt', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: tokens.standardAccessTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed + } + } }); }); it('returns access_token using sessionToken with authorization server', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -570,13 +608,19 @@ describe('token.getWithoutPrompt', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerAccessTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.authServerAccessTokenParsed + } + } }); }); it('returns access_token and id_token with an authorization server', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -607,13 +651,20 @@ describe('token.getWithoutPrompt', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: [tokens.authServerIdTokenParsed, tokens.authServerAccessTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed, + accessToken: tokens.authServerAccessTokenParsed + } + } }); }); - it('returns id_token and access_token (in that order) using an array of responseTypes', function() { + it('returns id_token and access_token using an array of responseTypes (in that order)', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -644,13 +695,20 @@ describe('token.getWithoutPrompt', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: [tokens.standardIdTokenParsed, tokens.standardAccessTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.standardIdTokenParsed, + accessToken: tokens.standardAccessTokenParsed + } + } }); }); - it('returns access_token and id_token (in that order) using an array of responseTypes', function() { + it('returns access_token and id_token using an array of responseTypes (in that order)', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -681,13 +739,20 @@ describe('token.getWithoutPrompt', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: [tokens.standardAccessTokenParsed, tokens.standardIdTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed, + idToken: tokens.standardIdTokenParsed + } + } }); }); it('returns a single token using an array with a single responseType', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -710,13 +775,19 @@ describe('token.getWithoutPrompt', function() { 'sessionToken': 'testSessionToken' } }, - expectedResp: [tokens.standardIdTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.standardIdTokenParsed + } + } }); }); oauthUtil.itpErrorsCorrectly('throws an error if multiple responseTypes are sent as a string', { oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -743,6 +814,35 @@ describe('token.getWithPopup', function() { jest.useRealTimers(); }); + it('promise will reject if extra options object is passed', function() { + return oauthUtil.setup({ + willFail: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com', + }, + getWithPopupArgs: [{ + /* expected options */ + }, { + /* extra options */ + }] + }) + .then(function() { + expect(true).toEqual(false); + }) + .catch(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'As of version 3.0, "getWithPopup" takes only a single set of options', + errorCode: 'INTERNAL', + errorSummary: 'As of version 3.0, "getWithPopup" takes only a single set of options', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + }); + }); + it('promise will reject if fails due to timeout', function() { var timeoutMs = 120000; var mockWindow = { @@ -752,12 +852,12 @@ describe('token.getWithPopup', function() { jest.spyOn(window, 'open').mockImplementation(function () { return mockWindow; // valid window is returned }); - jest.spyOn(Q.makePromise.prototype, 'timeout'); jest.useFakeTimers(); var promise = oauthUtil.setup({ closePopup: true, // prevent any message being passed willFail: true, oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -769,7 +869,7 @@ describe('token.getWithPopup', function() { .then(function() { expect(true).toEqual(false); }) - .fail(function(err) { + .catch(function(err) { expect(mockWindow.close).toHaveBeenCalled(); util.expectErrorToEqual(err, { name: 'AuthSdkError', @@ -784,7 +884,6 @@ describe('token.getWithPopup', function() { return Promise.resolve() .then(function() { jest.runAllTicks(); // resolve pending promises - expect(Q.makePromise.prototype.timeout).toHaveBeenCalled(); jest.advanceTimersByTime(timeoutMs); // should trigger timeout return promise; }); @@ -793,15 +892,13 @@ describe('token.getWithPopup', function() { jest.spyOn(window, 'open').mockImplementation(function () { return null; // null window is returned }); - jest.spyOn(Q.makePromise.prototype, 'timeout').mockImplementation(function() { - return this; // return for chaining promise methods - }); jest.useFakeTimers(); var promise = oauthUtil.setup({ closePopup: true, willFail: true, oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -813,7 +910,7 @@ describe('token.getWithPopup', function() { .then(function() { expect(true).toEqual(false); }) - .fail(function(err) { + .catch(function(err) { util.expectErrorToEqual(err, { name: 'AuthSdkError', message: 'Unable to parse OAuth flow response', @@ -831,9 +928,10 @@ describe('token.getWithPopup', function() { }); }); - it('returns id_token using idp', function() { + it('returns tokens using idp', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -846,7 +944,7 @@ describe('token.getWithPopup', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'display': 'popup', 'state': oauthUtil.mockedState, @@ -858,9 +956,10 @@ describe('token.getWithPopup', function() { }); }); - it('returns id_token using idp with authorization server', function() { + it('returns tokens using idp with authorization server', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -873,7 +972,7 @@ describe('token.getWithPopup', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'display': 'popup', 'state': oauthUtil.mockedState, @@ -883,23 +982,29 @@ describe('token.getWithPopup', function() { } }, postMessageResp: { + 'access_token': tokens.authServerAccessToken, 'id_token': tokens.authServerIdToken, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerIdTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed + } + } }); }); it('allows passing issuer through getWithPopup, which takes precedence', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/ORIGINAL_AUTH_SERVER_ID', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' }, getWithPopupArgs: [{ - idp: 'testIdp' - }, { + idp: 'testIdp', issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' }], postMessageSrc: { @@ -907,7 +1012,7 @@ describe('token.getWithPopup', function() { queryParams: { 'client_id': 'NPSfOkH5eZrTy8PMDlvx', 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token', + 'response_type': 'token id_token', 'response_mode': 'okta_post_message', 'display': 'popup', 'state': oauthUtil.mockedState, @@ -917,24 +1022,34 @@ describe('token.getWithPopup', function() { } }, postMessageResp: { + 'access_token': tokens.authServerAccessToken, 'id_token': tokens.authServerIdToken, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerIdTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed + } + } }); }); it('allows multiple popups simultaneously', function() { - jest.useFakeTimers(); + var firstPopup; + var secondPopup; + + // mock popup creation + var popups = []; + function getOpenPopups() { + return popups.filter(function(popup) { + return !popup.closed; + }); + } + return oauthUtil.setupSimultaneousPostMessage() .then(function(context) { - // mock popup creation - var popups = []; - function getOpenPopups() { - return popups.filter(function(popup) { - return !popup.closed; - }); - } + function FakePopup() { var popup = this; popup.closed = false; @@ -949,20 +1064,24 @@ describe('token.getWithPopup', function() { }); // getWithPopup, but don't resolve - var firstPopup = context.client.token.getWithPopup({ + firstPopup = context.client.token.getWithPopup({ idp: 'testIdp', + responseType: 'id_token', state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce }); // getWithPopup, but don't resolve - var secondPopup = context.client.token.getWithPopup({ + secondPopup = context.client.token.getWithPopup({ idp: 'testIdp2', + responseType: 'id_token', state: oauthUtil.mockedState2, nonce: oauthUtil.mockedNonce2 }); - - jest.runAllTicks(); // resolve promises + return waitFor(() => { + return popups.length === 2 ? context : false; + }) + }).then(context => { // assert that two popups are open expect(getOpenPopups().length).toBe(2); @@ -971,10 +1090,10 @@ describe('token.getWithPopup', function() { context.emitter.emit('trigger', oauthUtil.mockedState); context.emitter.emit('trigger', oauthUtil.mockedState2); - return Q.all([firstPopup, secondPopup]) - .spread(function(firstToken, secondToken) { - expect(firstToken).toEqual(tokens.standardIdTokenParsed); - expect(secondToken).toEqual(tokens.standardIdToken2Parsed); + return Promise.all([firstPopup, secondPopup]) + .then(function(values) { + expect(values[0].tokens.idToken).toEqual(tokens.standardIdTokenParsed); + expect(values[1].tokens.idToken).toEqual(tokens.standardIdToken2Parsed); // make sure both popups were closed expect(getOpenPopups().length).toBe(0); @@ -985,6 +1104,7 @@ describe('token.getWithPopup', function() { it('returns access_token using sessionToken', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1014,13 +1134,19 @@ describe('token.getWithPopup', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: tokens.standardAccessTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.standardAccessTokenParsed + } + } }); }); it('returns access_token using idp with authorization server', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1050,13 +1176,19 @@ describe('token.getWithPopup', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: tokens.authServerAccessTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerAccessTokenParsed + } + } }); }); it('returns access_token and id_token using idp with authorization server', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1087,13 +1219,20 @@ describe('token.getWithPopup', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: [tokens.authServerAccessTokenParsed, tokens.authServerIdTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed, + accessToken: tokens.authServerAccessTokenParsed + } + } }); }); - it('returns access_token and id_token (in that order) using idp', function() { + it('returns access_token and id_token using idp', function() { return oauthUtil.setupPopup({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1124,44 +1263,13 @@ describe('token.getWithPopup', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: [tokens.standardAccessTokenParsed, tokens.standardIdTokenParsed] - }); - }); - - it('returns id_token and access_token (in that order) using idp', function() { - return oauthUtil.setupPopup({ - oktaAuthArgs: { - issuer: 'https://auth-js-test.okta.com', - clientId: 'NPSfOkH5eZrTy8PMDlvx', - redirectUri: 'https://example.com/redirect' - }, - getWithPopupArgs: { - responseType: ['id_token', 'token'], - idp: 'testIdp' - }, - postMessageSrc: { - baseUri: 'https://auth-js-test.okta.com/oauth2/v1/authorize', - queryParams: { - 'client_id': 'NPSfOkH5eZrTy8PMDlvx', - 'redirect_uri': 'https://example.com/redirect', - 'response_type': 'id_token token', - 'response_mode': 'okta_post_message', - 'display': 'popup', - 'state': oauthUtil.mockedState, - 'nonce': oauthUtil.mockedNonce, - 'scope': 'openid email', - 'idp': 'testIdp' + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.standardIdTokenParsed, + accessToken: tokens.standardAccessTokenParsed } - }, - time: 1449699929, - postMessageResp: { - 'id_token': tokens.standardIdToken, - 'access_token': tokens.standardAccessToken, - 'token_type': 'Bearer', - 'expires_in': 3600, - 'state': oauthUtil.mockedState - }, - expectedResp: [tokens.standardIdTokenParsed, tokens.standardAccessTokenParsed] + } }); }); }); @@ -1173,8 +1281,21 @@ describe('token.getWithRedirect', function() { var customUrls; var nonceCookie; var stateCookie; + var originalLocation; + + afterEach(() => { + global.window.location = originalLocation; + }); beforeEach(function() { + // mock window.location so we appear to be on an HTTPS origin + originalLocation = global.window.location; + delete global.window.location; + global.window.location = { + protocol: 'https:', + hostname: 'somesite.local' + }; + defaultUrls = { issuer: 'https://auth-js-test.okta.com', authorizeUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize', @@ -1196,6 +1317,7 @@ describe('token.getWithRedirect', function() { oauthUtil.mockedNonce, null, // expiresAt { + secure: true, sameSite: 'none' } ]; @@ -1205,30 +1327,63 @@ describe('token.getWithRedirect', function() { oauthUtil.mockedState, null, // expiresAt { + secure: true, sameSite: 'none' } ]; }); function mockPKCE() { spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); - spyOn(sdkUtil, 'getWellKnown').and.returnValue(Q.resolve({ + spyOn(sdkUtil, 'getWellKnown').and.returnValue(Promise.resolve({ 'code_challenge_methods_supported': [codeChallengeMethod] })); spyOn(pkce, 'generateVerifier'); spyOn(pkce, 'saveMeta'); - spyOn(pkce, 'computeChallenge').and.returnValue(Q.resolve(codeChallenge)); + spyOn(pkce, 'computeChallenge').and.returnValue(Promise.resolve(codeChallenge)); } - it('Can pass responseMode=query', function() { + it('If extra options are passed, promise will reject', function() { + return oauthUtil.setupRedirect({ + willFail: true, + oktaAuthArgs: { + issuer: 'https://auth-js-test.okta.com', + }, + getWithRedirectArgs: [{ + /* expected options */ + }, { + /* extra options */ + }] + }) + .then(function() { + expect(true).toEqual(false); + }) + .catch(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'As of version 3.0, "getWithRedirect" takes only a single set of options', + errorCode: 'INTERNAL', + errorSummary: 'As of version 3.0, "getWithRedirect" takes only a single set of options', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + }); + }); + + it('PKCE: Can pass responseMode=fragment', function() { + mockPKCE(); return oauthUtil.setupRedirect({ + oktaAuthArgs: { + pkce: true + }, getWithRedirectArgs: { - responseMode: 'query', + responseMode: 'fragment', }, expectedCookies: [ [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: 'code', state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1237,7 +1392,8 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { - sameSite: 'none' + sameSite: 'none', + secure: true } ], nonceCookie, @@ -1245,29 +1401,30 @@ describe('token.getWithRedirect', function() { ], expectedRedirectUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize?' + 'client_id=NPSfOkH5eZrTy8PMDlvx&' + + 'code_challenge=' + codeChallenge + '&' + + 'code_challenge_method=' + codeChallengeMethod + '&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=query&' + - 'response_type=id_token&' + + 'response_mode=fragment&' + + 'response_type=code&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); }); - it('Can set responseMode=query on SDK instance', function() { + it('PKCE: Can set responseMode=fragment on SDK instance', function() { + mockPKCE(); return oauthUtil.setupRedirect({ oktaAuthArgs: { - issuer: 'https://auth-js-test.okta.com', - clientId: 'NPSfOkH5eZrTy8PMDlvx', - redirectUri: 'https://example.com/redirect', - responseMode: 'query' + pkce: true, + responseMode: 'fragment' }, getWithRedirectArgs: {}, expectedCookies: [ [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: 'code', state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1276,7 +1433,8 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { - sameSite: 'none' + sameSite: 'none', + secure: true } ], nonceCookie, @@ -1284,16 +1442,18 @@ describe('token.getWithRedirect', function() { ], expectedRedirectUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize?' + 'client_id=NPSfOkH5eZrTy8PMDlvx&' + + 'code_challenge=' + codeChallenge + '&' + + 'code_challenge_method=' + codeChallengeMethod + '&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=query&' + - 'response_type=id_token&' + - 'state=' + oauthUtil.mockedState + '&' + + 'response_mode=fragment&' + + 'response_type=code&' + + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); }); - it('sets authorize url and cookie for id_token using sessionToken', function() { + it('sets authorize url and cookie using sessionToken', function() { return oauthUtil.setupRedirect({ getWithRedirectArgs: { sessionToken: 'testToken' @@ -1302,7 +1462,7 @@ describe('token.getWithRedirect', function() { [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1311,6 +1471,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1321,17 +1482,17 @@ describe('token.getWithRedirect', function() { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + - 'response_type=id_token&' + + 'response_type=token%20id_token&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); }); - it('sets authorize url and cookie for id_token using sessionToken and authorization server', function() { + it('sets authorize url and cookie using sessionToken and authorization server', function() { return oauthUtil.setupRedirect({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1343,7 +1504,7 @@ describe('token.getWithRedirect', function() { [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1352,6 +1513,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1362,8 +1524,7 @@ describe('token.getWithRedirect', function() { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + - 'response_type=id_token&' + + 'response_type=token%20id_token&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' @@ -1373,6 +1534,7 @@ describe('token.getWithRedirect', function() { it('allows passing issuer through getWithRedirect, which takes precedence', function() { return oauthUtil.setupRedirect({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/ORIGINAL_AUTH_SERVER_ID', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1380,8 +1542,7 @@ describe('token.getWithRedirect', function() { getWithRedirectArgs: [{ responseType: 'token', scopes: ['email'], - sessionToken: 'testToken' - }, { + sessionToken: 'testToken', issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' }], expectedCookies: [ @@ -1397,6 +1558,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1407,7 +1569,6 @@ describe('token.getWithRedirect', function() { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=token&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1435,6 +1596,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1445,7 +1607,6 @@ describe('token.getWithRedirect', function() { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=token&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1456,6 +1617,7 @@ describe('token.getWithRedirect', function() { it('sets authorize url and cookie for access_token using sessionToken and authorization server', function() { return oauthUtil.setupRedirect({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1478,6 +1640,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1488,7 +1651,6 @@ describe('token.getWithRedirect', function() { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=token&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1515,6 +1677,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1526,7 +1689,6 @@ describe('token.getWithRedirect', function() { 'idp=testIdp&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=token%20id_token&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' @@ -1536,6 +1698,7 @@ describe('token.getWithRedirect', function() { it('sets authorize url for access_token and id_token using idp and authorization server', function() { return oauthUtil.setupRedirect({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1557,6 +1720,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1568,14 +1732,13 @@ describe('token.getWithRedirect', function() { 'idp=testIdp&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=token%20id_token&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); }); - it('sets authorize url for authorization code requests, defaulting responseMode to query', function() { + it('sets authorize url for authorization code requests', function() { return oauthUtil.setupRedirect({ getWithRedirectArgs: { sessionToken: 'testToken', @@ -1594,6 +1757,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1604,7 +1768,6 @@ describe('token.getWithRedirect', function() { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=query&' + 'response_type=code&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1612,7 +1775,7 @@ describe('token.getWithRedirect', function() { }); }); - it('PKCE: sets authorize url for authorization code requests, defaulting responseMode to fragment', function() { + it('PKCE: sets authorize url for authorization code requests', function() { mockPKCE(); return oauthUtil.setupRedirect({ oktaAuthArgs: { @@ -1620,7 +1783,6 @@ describe('token.getWithRedirect', function() { }, getWithRedirectArgs: { sessionToken: 'testToken', - responseType: 'code' }, expectedCookies: [ [ @@ -1635,6 +1797,7 @@ describe('token.getWithRedirect', function() { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1647,50 +1810,6 @@ describe('token.getWithRedirect', function() { 'code_challenge_method=' + codeChallengeMethod + '&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + - 'response_type=code&' + - 'sessionToken=testToken&' + - 'state=' + oauthUtil.mockedState + '&' + - 'scope=openid%20email' - }); - }); - - it('PKCE: can use grantType="authorization_code" as an alias for pkce: true', function() { - mockPKCE(); - return oauthUtil.setupRedirect({ - oktaAuthArgs: { - grantType: "authorization_code", // alias for pkce: true - }, - getWithRedirectArgs: { - sessionToken: 'testToken', - responseType: 'code' - }, - expectedCookies: [ - [ - 'okta-oauth-redirect-params', - JSON.stringify({ - responseType: 'code', - state: oauthUtil.mockedState, - nonce: oauthUtil.mockedNonce, - scopes: ['openid', 'email'], - clientId: 'NPSfOkH5eZrTy8PMDlvx', - urls: defaultUrls, - ignoreSignature: false - }), -null, { - sameSite: 'none' - } - ], - nonceCookie, - stateCookie - ], - expectedRedirectUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize?' + - 'client_id=NPSfOkH5eZrTy8PMDlvx&' + - 'code_challenge=' + codeChallenge + '&' + - 'code_challenge_method=' + codeChallengeMethod + '&' + - 'nonce=' + oauthUtil.mockedNonce + '&' + - 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=code&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1699,9 +1818,9 @@ null, { }); it('sets authorize url for authorization code requests with an authorization server', function() { - mockPKCE(); return oauthUtil.setupRedirect({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -1723,6 +1842,7 @@ null, { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1733,7 +1853,6 @@ null, { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=query&' + 'response_type=code&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1761,6 +1880,7 @@ null, { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1771,7 +1891,6 @@ null, { 'client_id=NPSfOkH5eZrTy8PMDlvx&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=query&' + 'response_type=code&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1803,6 +1922,7 @@ null, { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1815,7 +1935,6 @@ null, { 'code_challenge_method=' + codeChallengeMethod + '&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + 'response_type=code&' + 'sessionToken=testToken&' + 'state=' + oauthUtil.mockedState + '&' + @@ -1843,6 +1962,7 @@ null, { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1870,7 +1990,7 @@ null, { [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1879,6 +1999,7 @@ null, { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1890,8 +2011,7 @@ null, { 'login_hint=JoeUser&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + - 'response_type=id_token&' + + 'response_type=token%20id_token&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); @@ -1906,7 +2026,7 @@ null, { [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1915,6 +2035,7 @@ null, { ignoreSignature: false }), null, { + secure: true, sameSite: 'none' } ], @@ -1926,8 +2047,7 @@ null, { 'idp_scope=scope1%20scope2&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + - 'response_type=id_token&' + + 'response_type=token%20id_token&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); @@ -1942,7 +2062,7 @@ null, { [ 'okta-oauth-redirect-params', JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: oauthUtil.mockedState, nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], @@ -1950,7 +2070,8 @@ null, { urls: defaultUrls, ignoreSignature: false }), -null, { + null, { + secure: true, sameSite: 'none' } ], @@ -1962,8 +2083,7 @@ null, { 'idp_scope=scope1%20scope2&' + 'nonce=' + oauthUtil.mockedNonce + '&' + 'redirect_uri=https%3A%2F%2Fexample.com%2Fredirect&' + - 'response_mode=fragment&' + - 'response_type=id_token&' + + 'response_type=token%20id_token&' + 'state=' + oauthUtil.mockedState + '&' + 'scope=openid%20email' }); @@ -1972,12 +2092,26 @@ null, { }); describe('token.parseFromUrl', function() { + function mockPKCE(response) { + var codeVerifier = 'fake'; + var redirectUri = 'https://example.com/redirect'; + + spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true); + spyOn(pkce, 'loadMeta').and.returnValue({ + codeVerifier, + redirectUri + }); + spyOn(pkce, 'clearMeta'); + spyOn(pkce, 'getToken').and.returnValue(Promise.resolve(response)); + } + it('does not change the hash if a url is passed directly', function() { return oauthUtil.setupParseUrl({ parseFromUrlArgs: 'http://example.com#id_token=' + tokens.standardIdToken + - '&state=' + oauthUtil.mockedState, + '&access_token=' + tokens.standardAccessToken + + '&state=' + oauthUtil.mockedState, oauthCookie: JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', scopes: ['openid', 'email'], @@ -1989,10 +2123,11 @@ describe('token.parseFromUrl', function() { } }), expectedResp: { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed, + idToken: tokens.standardIdTokenParsed + } } }); }); @@ -2001,10 +2136,11 @@ describe('token.parseFromUrl', function() { return oauthUtil.setupParseUrl({ parseFromUrlArgs: { url: 'http://example.com#id_token=' + tokens.standardIdToken + - '&state=' + oauthUtil.mockedState, + '&access_token=' + tokens.standardAccessToken + + '&state=' + oauthUtil.mockedState, }, oauthCookie: JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', scopes: ['openid', 'email'], @@ -2016,23 +2152,28 @@ describe('token.parseFromUrl', function() { } }), expectedResp: { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed, + idToken: tokens.standardIdTokenParsed + } } }); }); - it('Can pass responseMode=query in options', function() { + it('PKCE: can parse code in query', function() { + mockPKCE({ + id_token: tokens.standardIdToken, + access_token: tokens.standardAccessToken + }); return oauthUtil.setupParseUrl({ - parseFromUrlArgs: { - responseMode: 'query' + oktaAuthArgs: { + pkce: true }, - searchMock: '?id_token=' + tokens.standardIdToken + - '&state=' + oauthUtil.mockedState, + searchMock: '?code=fake' + + '&state=' + oauthUtil.mockedState, oauthCookie: JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', scopes: ['openid', 'email'], @@ -2044,26 +2185,31 @@ describe('token.parseFromUrl', function() { } }), expectedResp: { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed, + idToken: tokens.standardIdTokenParsed + } } }); }); - it('Can set responseMode=query in SDK options', function() { + it('PKCE: can pass responseMode=fragment in options', function() { + mockPKCE({ + id_token: tokens.standardIdToken, + access_token: tokens.standardAccessToken + }) return oauthUtil.setupParseUrl({ oktaAuthArgs: { - url: 'https://auth-js-test.okta.com', - clientId: 'NPSfOkH5eZrTy8PMDlvx', - redirectUri: 'https://example.com/redirect', - responseMode: 'query' + pkce: true }, - searchMock: '?id_token=' + tokens.standardIdToken + - '&state=' + oauthUtil.mockedState, + parseFromUrlArgs: { + responseMode: 'fragment' + }, + hashMock: '#code=fake' + + '&state=' + oauthUtil.mockedState, oauthCookie: JSON.stringify({ - responseType: 'id_token', + responseType: ['token', 'id_token'], state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', scopes: ['openid', 'email'], @@ -2075,10 +2221,49 @@ describe('token.parseFromUrl', function() { } }), expectedResp: { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed, + idToken: tokens.standardIdTokenParsed + } + } + }); + }); + + it('PKCE: Can set responseMode=fragment in SDK options', function() { + mockPKCE({ + id_token: tokens.standardIdToken, + access_token: tokens.standardAccessToken + }); + return oauthUtil.setupParseUrl({ + oktaAuthArgs: { + pkce: true, + responseMode: 'fragment' + }, + hashMock: '#code=fake' + + '&state=' + oauthUtil.mockedState, + oauthCookie: JSON.stringify({ + responseType: ['token', 'id_token'], + state: oauthUtil.mockedState, + nonce: oauthUtil.mockedNonce, + scopes: ['openid', 'email'], + urls: { + issuer: 'https://auth-js-test.okta.com', + tokenUrl: 'https://auth-js-test.okta.com/oauth2/v1/token', + authorizeUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize', + userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo' + } + }), + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: { + idToken: tokens.standardIdToken, + claims: tokens.standardIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'email'] + } + } } }); }); @@ -2090,8 +2275,8 @@ describe('token.parseFromUrl', function() { '&state=' + oauthUtil.mockedState, oauthCookie: JSON.stringify({ responseType: 'id_token', - state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + state: oauthUtil.mockedState, + nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], urls: { issuer: 'https://auth-js-test.okta.com', @@ -2101,10 +2286,15 @@ describe('token.parseFromUrl', function() { } }), expectedResp: { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + idToken: { + idToken: tokens.standardIdToken, + claims: tokens.standardIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'email'] + } + } } }); }); @@ -2115,8 +2305,8 @@ describe('token.parseFromUrl', function() { '&state=' + oauthUtil.mockedState, oauthCookie: JSON.stringify({ responseType: 'id_token', - state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + state: oauthUtil.mockedState, + nonce: oauthUtil.mockedNonce, scopes: ['openid', 'email'], urls: { issuer: 'https://auth-js-test.okta.com', @@ -2126,10 +2316,15 @@ describe('token.parseFromUrl', function() { } }), expectedResp: { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + idToken: { + idToken: tokens.standardIdToken, + claims: tokens.standardIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'email'] + } + } } }); }); @@ -2150,7 +2345,12 @@ describe('token.parseFromUrl', function() { userinfoUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo' } }), - expectedResp: tokens.authServerIdTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + idToken: tokens.authServerIdTokenParsed + } + } }); }); @@ -2173,7 +2373,12 @@ describe('token.parseFromUrl', function() { userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo' } }), - expectedResp: tokens.standardAccessTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed + } + } }); }); @@ -2196,7 +2401,12 @@ describe('token.parseFromUrl', function() { userinfoUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo' } }), - expectedResp: tokens.authServerAccessTokenParsed + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.authServerAccessTokenParsed + } + } }); }); @@ -2220,7 +2430,13 @@ describe('token.parseFromUrl', function() { userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo' } }), - expectedResp: [tokens.standardIdTokenParsed, tokens.standardAccessTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.standardAccessTokenParsed, + idToken: tokens.standardIdTokenParsed + } + } }); }); @@ -2244,7 +2460,13 @@ describe('token.parseFromUrl', function() { userinfoUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo' } }), - expectedResp: [tokens.authServerIdTokenParsed, tokens.authServerAccessTokenParsed] + expectedResp: { + state: oauthUtil.mockedState, + tokens: { + accessToken: tokens.authServerAccessTokenParsed, + idToken: tokens.authServerIdTokenParsed + } + } }); }); @@ -2360,12 +2582,71 @@ describe('token.parseFromUrl', function() { errorCauses: [] } ); + + oauthUtil.itpErrorsCorrectly('throws an error if access_token was not returned', { + setupMethod: oauthUtil.setupParseUrl, + hashMock: '#id_token=' + tokens.standardIdToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: JSON.stringify({ + responseType: ['id_token', 'token'], + state: oauthUtil.mockedState, + nonce: oauthUtil.mockedNonce, + scopes: ['openid', 'email'], + urls: { + issuer: 'https://auth-js-test.okta.com', + tokenUrl: 'https://auth-js-test.okta.com/oauth2/v1/token', + authorizeUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize', + userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo' + } + }) + }, + { + name: 'AuthSdkError', + message: 'Unable to parse OAuth flow response: response type "token" was requested but "access_token" was not returned.', + errorCode: 'INTERNAL', + errorSummary: 'Unable to parse OAuth flow response: response type "token" was requested but "access_token" was not returned.', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); + + oauthUtil.itpErrorsCorrectly('throws an error if id_token was not returned', { + setupMethod: oauthUtil.setupParseUrl, + hashMock: '#access_token=' + tokens.standardAccessToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: JSON.stringify({ + responseType: ['id_token', 'token'], + state: oauthUtil.mockedState, + nonce: oauthUtil.mockedNonce, + scopes: ['openid', 'email'], + urls: { + issuer: 'https://auth-js-test.okta.com', + tokenUrl: 'https://auth-js-test.okta.com/oauth2/v1/token', + authorizeUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize', + userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo' + } + }) + }, + { + name: 'AuthSdkError', + message: 'Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.', + errorCode: 'INTERNAL', + errorSummary: 'Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + }); }); describe('token.renew', function() { it('returns id_token', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -2383,13 +2664,15 @@ describe('token.renew', function() { 'scope': 'openid email', 'prompt': 'none' } - } + }, + expectedResp: tokens.standardIdTokenParsed }); }); it('returns id_token with authorization server', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -2413,19 +2696,14 @@ describe('token.renew', function() { 'id_token': tokens.authServerIdToken, 'state': oauthUtil.mockedState }, - expectedResp: { - idToken: tokens.authServerIdToken, - claims: tokens.authServerIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'custom'], - issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' - } + expectedResp: tokens.authServerIdTokenParsed }); }); it('returns access_token', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -2451,19 +2729,14 @@ describe('token.renew', function() { 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: { - accessToken: tokens.standardAccessToken, - expiresAt: 1449703529, - scopes: ['openid', 'email'], - tokenType: 'Bearer', - issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' - } + expectedResp: tokens.standardAccessTokenParsed }); }); it('returns access_token with authorization server', function() { return oauthUtil.setupFrame({ oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com/oauth2/wontusethisone', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -2484,24 +2757,19 @@ describe('token.renew', function() { }, time: 1449699929, postMessageResp: { - 'access_token': tokens.standardAccessToken, + 'access_token': tokens.authServerAccessToken, 'token_type': 'Bearer', 'expires_in': 3600, 'state': oauthUtil.mockedState }, - expectedResp: { - accessToken: tokens.standardAccessToken, - expiresAt: 1449703529, - scopes: ['openid', 'email'], - tokenType: 'Bearer', - issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7' - } + expectedResp: tokens.authServerAccessTokenParsed }); }); oauthUtil.itpErrorsCorrectly('throws an error if a non-token is passed', { oktaAuthArgs: { + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' @@ -2521,86 +2789,149 @@ describe('token.renew', function() { }); describe('token.getUserInfo', function() { + let responseXHR; + beforeEach(() => { + responseXHR = _.cloneDeep(require('@okta/test.support/xhr/userinfo')); + responseXHR.response.sub = tokens.standardIdTokenParsed.claims.sub; + }); + util.itMakesCorrectRequestResponse({ - title: 'allows retrieving UserInfo', - setup: { - request: { - uri: '/oauth2/v1/userinfo', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'X-Okta-User-Agent-Extended': 'okta-auth-js-' + packageJson.version, - 'Authorization': 'Bearer ' + tokens.standardAccessToken - } - }, - response: 'userinfo' + title: 'allows retrieving UserInfo with accessTokenObject and idTokenObject', + setup: () => { + return { + request: { + uri: '/oauth2/v1/userinfo', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': 'okta-auth-js-' + packageJson.version, + 'Authorization': 'Bearer ' + tokens.standardAccessToken + } + }, + response: responseXHR + } }, execute: function(test) { - return test.oa.token.getUserInfo(tokens.standardAccessTokenParsed); + return test.oa.token.getUserInfo(tokens.standardAccessTokenParsed, tokens.standardIdTokenParsed); }, expectations: function(test, res) { - expect(res).toEqual({ - 'sub': '00u15ozp26ACQTGHJEBH', - 'email': 'samljackson@example.com', - 'email_verified': true - }); + expect(res).toEqual(responseXHR.response); + } + }); + + + util.itMakesCorrectRequestResponse({ + title: 'allows retrieving UserInfo with no arguments if valid tokens exist in token manager', + setup: () => { + return { + request: { + uri: '/oauth2/v1/userinfo', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': 'okta-auth-js-' + packageJson.version, + 'Authorization': 'Bearer ' + tokens.standardAccessToken + } + }, + response: responseXHR + } + }, + execute: function(test) { + util.warpToUnixTime(oauthUtil.getTime()); + test.oa.tokenManager.add('accessToken', tokens.standardAccessTokenParsed); + test.oa.tokenManager.add('idToken', tokens.standardIdTokenParsed); + return test.oa.token.getUserInfo(); + }, + expectations: function(test, res) { + expect(res).toEqual(responseXHR.response); } }); util.itMakesCorrectRequestResponse({ title: 'allows retrieving UserInfo using authorization server', - setup: { - request: { - uri: '/oauth2/aus8aus76q8iphupD0h7/v1/userinfo', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'X-Okta-User-Agent-Extended': 'okta-auth-js-' + packageJson.version, - 'Authorization': 'Bearer ' + tokens.authServerAccessToken - } - }, - response: 'userinfo' + setup: () => { + responseXHR.response.sub = tokens.authServerIdTokenParsed.claims.sub; + return { + request: { + uri: '/oauth2/aus8aus76q8iphupD0h7/v1/userinfo', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': 'okta-auth-js-' + packageJson.version, + 'Authorization': 'Bearer ' + tokens.authServerAccessToken + } + }, + response: responseXHR + }; }, execute: function(test) { - return test.oa.token.getUserInfo(tokens.authServerAccessTokenParsed); + return test.oa.token.getUserInfo(tokens.authServerAccessTokenParsed, tokens.authServerIdTokenParsed); }, expectations: function(test, res) { - expect(res).toEqual({ - 'sub': '00u15ozp26ACQTGHJEBH', - 'email': 'samljackson@example.com', - 'email_verified': true - }); + expect(res).toEqual(responseXHR.response); } }); - it('throws an error if no arguments are passed instead', function() { - return Q.resolve(setupSync()) + it('throws an error if no arguments are passed', function() { + return Promise.resolve(setupSync()) .then(function(oa) { + jest.spyOn(oa.tokenManager, 'get').mockReturnValue(Promise.resolve()); return oa.token.getUserInfo(); }) .then(function() { expect('not to be hit').toBe(true); }) - .fail(function(err) { + .catch(function(err) { expect(err.name).toEqual('AuthSdkError'); expect(err.errorSummary).toBe('getUserInfo requires an access token object'); }); }); it('throws an error if a string is passed instead of an accessToken object', function() { - return Q.resolve(setupSync()) + return Promise.resolve(setupSync()) .then(function(oa) { + jest.spyOn(oa.tokenManager, 'get').mockReturnValue(Promise.resolve()); return oa.token.getUserInfo('just a string'); }) .then(function() { expect('not to be hit').toBe(true); }) - .fail(function(err) { + .catch(function(err) { expect(err.name).toEqual('AuthSdkError'); expect(err.errorSummary).toBe('getUserInfo requires an access token object'); }); }); + it('throws an error if no idTokenObject is passed', function() { + return Promise.resolve(setupSync()) + .then(function(oa) { + jest.spyOn(oa.tokenManager, 'get').mockReturnValue(Promise.resolve()); + return oa.token.getUserInfo(tokens.standardAccessTokenParsed); + }) + .then(function() { + expect('not to be hit').toBe(true); + }) + .catch(function(err) { + expect(err.name).toEqual('AuthSdkError'); + expect(err.errorSummary).toBe('getUserInfo requires an ID token object'); + }); + }); + + it('throws an error if a string is passed instead of an idTokenObject', function() { + return Promise.resolve(setupSync()) + .then(function(oa) { + jest.spyOn(oa.tokenManager, 'get').mockReturnValue(Promise.resolve()); + return oa.token.getUserInfo(tokens.standardAccessTokenParsed, 'some string'); + }) + .then(function() { + expect('not to be hit').toBe(true); + }) + .catch(function(err) { + expect(err.name).toEqual('AuthSdkError'); + expect(err.errorSummary).toBe('getUserInfo requires an ID token object'); + }); + }); + util.itErrorsCorrectly({ title: 'returns correct error for 403', setup: { @@ -2616,7 +2947,7 @@ describe('token.getUserInfo', function() { response: 'error-userinfo-insufficient-scope' }, execute: function(test) { - return test.oa.token.getUserInfo(tokens.standardAccessTokenParsed); + return test.oa.token.getUserInfo(tokens.standardAccessTokenParsed, tokens.standardIdTokenParsed); }, expectations: function(test, err) { expect(err.name).toEqual('OAuthError'); @@ -2643,7 +2974,7 @@ describe('token.getUserInfo', function() { response: 'error-userinfo-invalid-token' }, execute: function(test) { - return test.oa.token.getUserInfo(tokens.standardAccessTokenParsed); + return test.oa.token.getUserInfo(tokens.standardAccessTokenParsed, tokens.standardIdTokenParsed); }, expectations: function(test, err) { expect(err.name).toEqual('OAuthError'); @@ -2655,35 +2986,86 @@ describe('token.getUserInfo', function() { }); describe('token.verify', function() { - var validationParams = { - clientId: tokens.standardIdTokenParsed.clientId, - issuer: tokens.standardIdTokenParsed.issuer - }; + var validationParams; + var client; + beforeEach(() => { + validationParams = { + clientId: tokens.standardIdTokenParsed.clientId, + issuer: tokens.standardIdTokenParsed.issuer + }; + client = setupSync(); + }); + + describe('with access token', () => { + var idToken; + var atHash; + + beforeEach(() => { + atHash = 'Gryuqew1_irUBmgZAncMsA'; // based on tokens.standardAccessToken + + // Mock out sdk crypto + jest.spyOn(client.features, 'isTokenVerifySupported').mockReturnValue(true); + jest.spyOn(sdkCrypto, 'verifyToken').mockReturnValue(true); + jest.spyOn(sdkCrypto, 'getOidcHash').mockReturnValue(Promise.resolve(atHash)); + + // Return modified idToken + idToken = _.cloneDeep(tokens.standardIdTokenParsed); + idToken.claims.at_hash = atHash; + }); + + it('verifies idToken at_hash claim against accessToken', () => { + util.warpToUnixTime(1449699929); + oauthUtil.loadWellKnownAndKeysCache(); + validationParams.accessToken = tokens.standardAccessToken; + return client.token.verify(idToken, validationParams) + .then(function(res) { + expect(res).toEqual(idToken); + expect(sdkCrypto.getOidcHash).toHaveBeenCalledWith(tokens.standardAccessToken); + }); + }); + + it('throws if idToken at_hash claim does not match accessToken', () => { + util.warpToUnixTime(1449699929); + oauthUtil.loadWellKnownAndKeysCache(); + validationParams.accessToken = tokens.standardAccessToken; + idToken.claims.at_hash = 'other_hash'; + return client.token.verify(idToken, validationParams) + .then(function() { + expect('not to be hit').toEqual(true); + }) + .catch(function(err) { + util.assertAuthSdkError(err, 'Token hash verification failed'); + }); + }); + + it('skips verification if idToken does not have at_hash claim', () => { + util.warpToUnixTime(1449699929); + oauthUtil.loadWellKnownAndKeysCache(); + validationParams.accessToken = tokens.standardAccessToken; + delete idToken.claims.at_hash; + return client.token.verify(idToken, validationParams) + .then(function(res) { + expect(res).toEqual(idToken); + expect(sdkCrypto.getOidcHash).not.toHaveBeenCalled(); + }); + }); + }); it('verifies a valid idToken with nonce', function() { - var client = setupSync(); util.warpToUnixTime(1449699929); oauthUtil.loadWellKnownAndKeysCache(); - var alteredParams = _.clone(validationParams); - alteredParams.nonce = tokens.standardIdTokenParsed.nonce; + validationParams.nonce = tokens.standardIdTokenParsed.nonce; return client.token.verify(tokens.standardIdTokenParsed, validationParams) .then(function(res) { expect(res).toEqual(tokens.standardIdTokenParsed); - }) - .fail(function() { - expect('not to be hit').toEqual(true); - }) + }); }); - it('verifies a valid idToken without nonce', function() { - var client = setupSync(); + it('verifies a valid idToken without nonce or accessToken', function() { util.warpToUnixTime(1449699929); oauthUtil.loadWellKnownAndKeysCache(); return client.token.verify(tokens.standardIdTokenParsed, validationParams) .then(function(res) { expect(res).toEqual(tokens.standardIdTokenParsed); - }) - .fail(function() { - expect('not to be hit').toEqual(true); }); }); @@ -2695,12 +3077,11 @@ describe('token.verify', function() { jest.useRealTimers(); }); function expectError(verifyArgs, message) { - var client = setupSync(); return client.token.verify.apply(null, verifyArgs) .then(function() { expect('not to be hit').toEqual(true); }) - .fail(function(err) { + .catch(function(err) { util.assertAuthSdkError(err, message); }); } @@ -2720,21 +3101,18 @@ describe('token.verify', function() { 'The JWT expired and is no longer valid'); }); it('invalid nonce', function() { - var alteredParams = _.clone(validationParams); - alteredParams.nonce = 'invalidNonce'; - return expectError([tokens.standardIdToken2Parsed, alteredParams], + validationParams.nonce = 'invalidNonce'; + return expectError([tokens.standardIdToken2Parsed, validationParams], 'OAuth flow response nonce doesn\'t match request nonce'); }); it('invalid audience', function() { - var alteredParams = _.clone(validationParams); - alteredParams.clientId = 'invalidAudience'; - return expectError([tokens.standardIdTokenParsed, alteredParams], + validationParams.clientId = 'invalidAudience'; + return expectError([tokens.standardIdTokenParsed, validationParams], 'The audience [NPSfOkH5eZrTy8PMDlvx] does not match [invalidAudience]'); }); it('invalid issuer', function() { - var alteredParams = _.clone(validationParams); - alteredParams.issuer = 'http://invalidissuer.example.com'; - return expectError([tokens.standardIdTokenParsed, alteredParams], + validationParams.issuer = 'http://invalidissuer.example.com'; + return expectError([tokens.standardIdTokenParsed, validationParams], 'The issuer [https://auth-js-test.okta.com] does not match [http://invalidissuer.example.com]'); }); it('expired before issued', function() { diff --git a/packages/okta-auth-js/test/spec/tokenManager.js b/packages/okta-auth-js/test/spec/tokenManager.js index 5bd0186ba..f3d59b9cf 100644 --- a/packages/okta-auth-js/test/spec/tokenManager.js +++ b/packages/okta-auth-js/test/spec/tokenManager.js @@ -1,9 +1,9 @@ +/* global window, localStorage, sessionStorage */ + +// Promise.allSettled is added in Node 12.10 var allSettled = require('promise.allsettled'); allSettled.shim(); // will be a no-op if not needed -var promiseFinally = require('promise.prototype.finally'); -promiseFinally.shim(); // will be a no-op if not needed - var Emitter = require('tiny-emitter'); var OktaAuth = require('OktaAuth'); var tokens = require('@okta/test.support/tokens'); @@ -16,6 +16,7 @@ function setupSync(options) { options.tokenManager = options.tokenManager || {}; jest.spyOn(SdkClock, 'create').mockReturnValue(new SdkClock(options.localClockOffset)); return new OktaAuth({ + pkce: false, issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect', @@ -30,11 +31,21 @@ function setupSync(options) { } describe('TokenManager', function() { + let originalLocation; beforeEach(function() { localStorage.clear(); sessionStorage.clear(); + + // Mock window.location so we appear to be on an HTTPS origin + originalLocation = global.window.location; + delete global.window.location; + global.window.location = { + protocol: 'https:', + hostname: 'somesite.local' + }; }); afterEach(function() { + global.window.location = originalLocation; jest.useRealTimers(); }); @@ -198,7 +209,8 @@ describe('TokenManager', function() { 'okta-token-storage', JSON.stringify({'test-idToken': tokens.standardIdTokenParsed}), '2200-01-01T00:00:00.000Z', { - sameSite: 'none' + sameSite: 'none', + secure: true } ); }); @@ -221,6 +233,7 @@ describe('TokenManager', function() { 'okta-token-storage', JSON.stringify({'test-idToken': tokens.standardIdTokenParsed}), '2200-01-01T00:00:00.000Z', { + secure: true, sameSite: 'none' } ); @@ -450,7 +463,7 @@ describe('TokenManager', function() { .then(function() { expect(true).toEqual(false); }) - .fail(function(err) { + .catch(function(err) { util.expectErrorToEqual(err, { name: 'AuthSdkError', message: 'Unable to parse storage string: okta-token-storage', @@ -516,7 +529,7 @@ describe('TokenManager', function() { state: oauthUtil.mockedState } }) - .fail(function(e) { + .catch(function(e) { util.expectErrorToEqual(e, { name: 'OAuthError', message: 'something went wrong', @@ -547,7 +560,7 @@ describe('TokenManager', function() { state: oauthUtil.mockedState } }) - .fail(function(e) { + .catch(function(e) { util.expectErrorToEqual(e, { name: 'AuthSdkError', message: 'The request does not match client configuration', @@ -793,7 +806,7 @@ describe('TokenManager', function() { state: oauthUtil.mockedState } }) - .fail(function(err) { + .catch(function(err) { util.expectErrorToEqual(err, { name: 'OAuthError', message: 'something went wrong', @@ -846,7 +859,7 @@ describe('TokenManager', function() { state: oauthUtil.mockedState } }) - .fail(function(err) { + .catch(function(err) { util.expectErrorToEqual(err, { name: 'AuthSdkError', message: 'The request does not match client configuration', @@ -885,7 +898,7 @@ describe('TokenManager', function() { state: oauthUtil.mockedState } }) - .fail(function(err) { + .catch(function(err) { util.expectErrorToEqual(err, { name: 'OAuthError', message: 'something went wrong', @@ -920,7 +933,7 @@ describe('TokenManager', function() { state: oauthUtil.mockedState } }) - .fail(function(e) { + .catch(function(e) { util.expectErrorToEqual(e, { name: 'AuthSdkError', message: 'The request does not match client configuration', @@ -1223,12 +1236,10 @@ describe('TokenManager', function() { describe('cookie', function() { - function cookieStorageSetup(options) { - options = options || {}; + function cookieStorageSetup() { return setupSync({ tokenManager: { - storage: 'cookie', - secure: options.secure + storage: 'cookie' } }); } @@ -1238,19 +1249,6 @@ describe('TokenManager', function() { var client = cookieStorageSetup(); var setCookieMock = util.mockSetCookie(); client.tokenManager.add('test-idToken', tokens.standardIdTokenParsed); - expect(setCookieMock).toHaveBeenCalledWith( - 'okta-token-storage', - JSON.stringify({'test-idToken': tokens.standardIdTokenParsed}), - '2200-01-01T00:00:00.000Z', { - sameSite: 'none' - } - ); - }); - - it('respects the "secure" option', function() { - var client = cookieStorageSetup({ secure: true }); - var setCookieMock = util.mockSetCookie(); - client.tokenManager.add('test-idToken', tokens.standardIdTokenParsed); expect(setCookieMock).toHaveBeenCalledWith( 'okta-token-storage', JSON.stringify({'test-idToken': tokens.standardIdTokenParsed}), @@ -1265,27 +1263,50 @@ describe('TokenManager', function() { describe('get', function() { it('returns a token', function() { - var client = cookieStorageSetup(); - client.tokenManager.add('test-idToken', tokens.standardIdTokenParsed); - util.warpToUnixTime(tokens.standardIdTokenClaims.iat); + const setCookieMock = util.mockSetCookie(); + const getCookieMock = util.mockGetCookie(JSON.stringify({ + 'test-idToken': tokens.standardIdTokenParsed + })); + const client = cookieStorageSetup(); + util.warpToUnixTime(tokens.standardIdTokenClaims.iat); // token should not be expired return client.tokenManager.get('test-idToken') .then(function(token) { expect(token).toEqual(tokens.standardIdTokenParsed); + expect(getCookieMock).toHaveBeenCalledWith('okta-token-storage'); + expect(setCookieMock).not.toHaveBeenCalled(); }); }); it('returns undefined for an expired token', function() { - var client = cookieStorageSetup(); + const setCookieMock = util.mockSetCookie(); + const getCookieMock = util.mockGetCookie(JSON.stringify({ + 'test-idToken': tokens.standardIdTokenParsed + })); + const client = cookieStorageSetup(); + util.warpToUnixTime(tokens.standardIdTokenClaims.exp + 1); // token should be expired client.tokenManager.add('test-idToken', tokens.standardIdTokenParsed); return client.tokenManager.get('test-idToken') .then(function(token) { expect(token).toBeUndefined(); + expect(getCookieMock).toHaveBeenCalledWith('okta-token-storage'); + expect(setCookieMock).toHaveBeenCalledWith( + 'okta-token-storage', + '{}', + '2200-01-01T00:00:00.000Z', { + secure: true, + sameSite: 'none' + } + ); }); }); }); describe('remove', function() { it('removes a token', function() { + util.mockGetCookie(JSON.stringify({ + 'test-idToken': tokens.standardIdTokenParsed, + 'anotherKey': tokens.standardIdTokenParsed + })); var client = cookieStorageSetup(); client.tokenManager.add('test-idToken', tokens.standardIdTokenParsed); client.tokenManager.add('anotherKey', tokens.standardIdTokenParsed); @@ -1295,6 +1316,7 @@ describe('TokenManager', function() { 'okta-token-storage', JSON.stringify({anotherKey: tokens.standardIdTokenParsed}), '2200-01-01T00:00:00.000Z', { + secure: true, sameSite: 'none' } ); @@ -1303,6 +1325,10 @@ describe('TokenManager', function() { describe('clear', function() { it('clears all tokens', function() { + util.mockGetCookie(JSON.stringify({ + 'test-idToken': tokens.standardIdTokenParsed, + 'anotherKey': tokens.standardIdTokenParsed + })); var client = cookieStorageSetup(); client.tokenManager.add('test-idToken', tokens.standardIdTokenParsed); client.tokenManager.add('anotherKey', tokens.standardIdTokenParsed); @@ -1312,6 +1338,7 @@ describe('TokenManager', function() { 'okta-token-storage', '{}', '2200-01-01T00:00:00.000Z', { + secure: true, sameSite: 'none' } ); diff --git a/packages/okta-auth-js/test/spec/util.js b/packages/okta-auth-js/test/spec/util.js index d58dbf676..b4a4252b3 100644 --- a/packages/okta-auth-js/test/spec/util.js +++ b/packages/okta-auth-js/test/spec/util.js @@ -1,3 +1,4 @@ +/* global window, document */ var util = require('../../lib/util'); describe('util', function() { diff --git a/packages/okta-auth-js/webpack.common.config.js b/packages/okta-auth-js/webpack.common.config.js index 4e1920386..c5de0b833 100644 --- a/packages/okta-auth-js/webpack.common.config.js +++ b/packages/okta-auth-js/webpack.common.config.js @@ -6,6 +6,18 @@ module.exports = { module: { loaders: [ { test: /\.json$/, loader: 'json' } + ], + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + presets: ['@babel/env'], + plugins: ['@babel/plugin-transform-runtime'], + sourceType: 'unambiguous' + } + } ] }, plugins: [ diff --git a/packages/okta-auth-js/webpack.config.js b/packages/okta-auth-js/webpack.config.js index 76bfb9d4c..cb92035d6 100644 --- a/packages/okta-auth-js/webpack.config.js +++ b/packages/okta-auth-js/webpack.config.js @@ -1,7 +1,6 @@ /* * This config builds a minified version that can be imported - * anywhere without any dependencies. It packages the SDK - * with reqwest and Q. It also preserves license comments. + * anywhere without any dependencies. It also preserves license comments. */ /* global __dirname */ var path = require('path'); diff --git a/test/app/env.js b/test/app/env.js new file mode 100644 index 000000000..c343de8fc --- /dev/null +++ b/test/app/env.js @@ -0,0 +1,15 @@ +/* global process, __dirname */ +const dotenv = require('dotenv'); +const fs = require('fs'); +const path = require('path'); +const ROOT_DIR = path.resolve(__dirname, '..', '..'); + +// Read environment variables from "testenv". Override environment vars if they are already set. +const TESTENV = path.resolve(ROOT_DIR, 'testenv'); + +if (fs.existsSync(TESTENV)) { + const envConfig = dotenv.parse(fs.readFileSync(TESTENV)); + Object.keys(envConfig).forEach((k) => { + process.env[k] = envConfig[k]; + }); +} diff --git a/test/app/package.json b/test/app/package.json index 28e36c82d..11d816222 100644 --- a/test/app/package.json +++ b/test/app/package.json @@ -6,22 +6,21 @@ "scripts": { "lint": "eslint .", "watch": "webpack --watch", - "start": "webpack-dev-server", - "start:dev": "yarn start --open", + "start": "node server.js", "build": "webpack", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "Apache-2.0", "dependencies": { - "dotenv": "^7.0.0", - "webpack": "^4.29.6" - }, - "devDependencies": { "@webpack-cli/serve": "^0.1.5", + "dotenv": "^7.0.0", + "express": "^4.17.1", "source-map-loader": "^0.2.4", "text-encoding": "^0.7.0", + "webpack": "^4.29.6", "webpack-cli": "^3.3.0", + "webpack-dev-middleware": "^3.7.2", "webpack-dev-server": "^3.2.1" } } diff --git a/test/app/public/index.html b/test/app/public/index.html index a1f9517ef..d740bfa24 100644 --- a/test/app/public/index.html +++ b/test/app/public/index.html @@ -8,5 +8,6 @@ + Server-side Login Form \ No newline at end of file diff --git a/test/app/public/server/index.html b/test/app/public/server/index.html new file mode 100644 index 000000000..f46e856f8 --- /dev/null +++ b/test/app/public/server/index.html @@ -0,0 +1,40 @@ + + + + + +
+ Server-side Login Form +
+
+
+
+
+ +
+
+
+
+ Status
+
+
+
+ Session Token
+
+
+
+ Error
+
+
+
+
+ + Back to SPA app + + \ No newline at end of file diff --git a/test/app/server.js b/test/app/server.js new file mode 100644 index 000000000..e8d005b12 --- /dev/null +++ b/test/app/server.js @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ + +require('./env'); // update environment variables from testenv file + +const OktaAuthJS = require('@okta/okta-auth-js'); + +const util = require('./src/util'); +const express = require('express'); +const webpack = require('webpack'); +const webpackDevMiddleware = require('webpack-dev-middleware'); + +const app = express(); +const config = require('./webpack.config.js'); +const compiler = webpack(config); + +// Tell express to use the webpack-dev-middleware and use the webpack.config.js +// configuration file as a base. +app.use(webpackDevMiddleware(compiler, { + publicPath: config.output.publicPath, +})); + +app.use(express.static('./public')); + +app.use(express.urlencoded()); +app.post('/login', function(req, res) { + const issuer = req.body.issuer; + const username = req.body.username; + const password = req.body.password; + let status = ''; + let sessionToken = ''; + let error = ''; + let authClient; + + try { + authClient = new OktaAuthJS({ + issuer + }); + } catch(e) { + console.error('Caught exception in OktaAuthJS constructor: ', e); + } + + authClient.signIn({ + username, + password + }) + .then(function(transaction) { + status = transaction.status; + sessionToken = transaction.sessionToken; + }) + .catch(function(err) { + error = err; + console.error(error); + }) + .finally(function() { + const qs = util.toQueryParams({ + status, + sessionToken, + error + }); + res.redirect('/server' + qs); + }); +}); + +const port = config.devServer.port; +app.listen(port, function () { + console.log(`Test app running at http://localhost/${port}!\n`); +}); diff --git a/test/app/src/config.js b/test/app/src/config.js index f836d912d..0e60c8360 100644 --- a/test/app/src/config.js +++ b/test/app/src/config.js @@ -3,6 +3,7 @@ import { CALLBACK_PATH, STORAGE_KEY } from './constants'; const HOST = window.location.host; const PROTO = window.location.protocol; const REDIRECT_URI = `${PROTO}//${HOST}${CALLBACK_PATH}`; +const POST_LOGOUT_REDIRECT_URI = `${PROTO}//${HOST}`; function getDefaultConfig() { const ISSUER = process.env.ISSUER; @@ -10,39 +11,51 @@ function getDefaultConfig() { return { redirectUri: REDIRECT_URI, + postLogoutRedirectUri: POST_LOGOUT_REDIRECT_URI, issuer: ISSUER, clientId: CLIENT_ID, + responseType: ['token', 'id_token'], pkce: true, + secureCookies: true }; } function getConfigFromUrl() { const url = new URL(window.location.href); const issuer = url.searchParams.get('issuer'); + const redirectUri = url.searchParams.get('redirectUri') || REDIRECT_URI; + const postLogoutRedirectUri = url.searchParams.get('postLogoutRedirectUri') || POST_LOGOUT_REDIRECT_URI; const clientId = url.searchParams.get('clientId'); - const pkce = url.searchParams.get('pkce') && url.searchParams.get('pkce') !== 'false'; + const pkce = url.searchParams.get('pkce') !== 'false'; // On by default const scopes = (url.searchParams.get('scopes') || 'openid,email').split(','); const responseType = (url.searchParams.get('responseType') || 'id_token,token').split(','); const responseMode = url.searchParams.get('responseMode') || undefined; const storage = url.searchParams.get('storage') || undefined; - const secure = url.searchParams.get('secure') === 'on'; // currently opt-in. + const secureCookies = url.searchParams.get('secureCookies') !== 'false'; // On by default return { - redirectUri: REDIRECT_URI, + redirectUri, + postLogoutRedirectUri, issuer, clientId, pkce, scopes, responseType, responseMode, + secureCookies, tokenManager: { storage, - secure, }, }; } function saveConfigToStorage(config) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); + const configCopy = {}; + Object.keys(config).forEach(key => { + if (typeof config[key] !== 'function') { + configCopy[key] = config[key]; + } + }); + localStorage.setItem(STORAGE_KEY, JSON.stringify(configCopy)); } function getConfigFromStorage() { diff --git a/test/app/src/form.js b/test/app/src/form.js index 294ff1a99..cd109f96d 100644 --- a/test/app/src/form.js +++ b/test/app/src/form.js @@ -5,13 +5,18 @@ const Form = `


-
+
+
+

+
+ ON
+ OFF

-
+
+ ON
+ OFF

@@ -29,11 +36,24 @@ const Form = ` function updateForm(config) { config = flattenConfig(config); document.getElementById('issuer').value = config.issuer; + document.getElementById('redirectUri').value = config.redirectUri; + document.getElementById('responseType').value = config.responseType.join(','); + document.getElementById('postLogoutRedirectUri').value = config.postLogoutRedirectUri; document.getElementById('clientId').value = config.clientId; - document.getElementById('pkce').checked = !!config.pkce; document.querySelector(`#responseMode [value="${config.responseMode || ''}"]`).selected = true; document.querySelector(`#storage [value="${config.storage || ''}"]`).selected = true; - document.getElementById('secure').checked = !!config.secure; + + if (config.pkce) { + document.getElementById('pkce-on').checked = true; + } else { + document.getElementById('pkce-off').checked = true; + } + + if (config.secureCookies) { + document.getElementById('secureCookies-on').checked = true; + } else { + document.getElementById('secureCookies-off').checked = true; + } } export { Form, updateForm }; diff --git a/test/app/src/testApp.js b/test/app/src/testApp.js index db6c1220c..f3a5fa5df 100644 --- a/test/app/src/testApp.js +++ b/test/app/src/testApp.js @@ -17,7 +17,7 @@ import { saveConfigToStorage, flattenConfig } from './config'; import { MOUNT_PATH } from './constants'; import { htmlString, toQueryParams } from './util'; import { Form, updateForm } from './form'; -import { tokensArrayToObject, tokensHTML } from './tokens'; +import { tokensHTML } from './tokens'; function homeLink(app) { return `Return Home`; @@ -25,9 +25,9 @@ function homeLink(app) { function logoutLink(app) { return ` - Logout (and reload)
- Logout (and redirect here)
- Logout (local only)
+ Logout (and redirect here)
+ Logout (XHR + reload)
+ Logout (app only)
`; } @@ -62,10 +62,9 @@ function bindFunctions(testApp, window) { loginDirect: testApp.loginDirect.bind(testApp), getToken: testApp.getToken.bind(testApp, {}), clearTokens: testApp.clearTokens.bind(testApp), - logout: testApp.logout.bind(testApp), - logoutAndReload: testApp.logoutAndReload.bind(testApp), - logoutAndRedirect: testApp.logoutAndRedirect.bind(testApp), - logoutLocal: testApp.logoutLocal.bind(testApp), + logoutRedirect: testApp.logoutRedirect.bind(testApp), + logoutXHR: testApp.logoutXHR.bind(testApp), + logoutApp: testApp.logoutApp.bind(testApp), refreshSession: testApp.refreshSession.bind(testApp), renewToken: testApp.renewToken.bind(testApp), revokeToken: testApp.revokeToken.bind(testApp), @@ -202,8 +201,8 @@ Object.assign(TestApp.prototype, { scopes: this.config.scopes, }, options); return this.oktaAuth.token.getWithPopup(options) - .then((tokens) => { - this.saveTokens(tokens); + .then(res => { + this.saveTokens(res.tokens); this.render(); }); }, @@ -213,8 +212,8 @@ Object.assign(TestApp.prototype, { scopes: this.config.scopes, }, options); return this.oktaAuth.token.getWithoutPrompt(options) - .then((tokens) => { - this.saveTokens(tokens); + .then(res => { + this.saveTokens(res.tokens); this.render(); }); }, @@ -222,8 +221,7 @@ Object.assign(TestApp.prototype, { return this.oktaAuth.session.refresh(); }, revokeToken: async function() { - const accessToken = await this.oktaAuth.tokenManager.get('accessToken'); - return this.oktaAuth.token.revoke(accessToken) + return this.oktaAuth.revokeAccessToken() .then(() => { document.getElementById('token-msg').innerHTML = 'access token revoked'; }); @@ -234,29 +232,25 @@ Object.assign(TestApp.prototype, { this.render(); }); }, - logout: async function() { - return this.oktaAuth.signOut(); - }, - logoutAndReload: function() { - this.logout() + logoutRedirect: function() { + this.oktaAuth.signOut() .catch(e => { - console.error('Error during signout: ', e); - }) - .then(() => { - window.location.reload(); + console.error('Error during signout & redirect: ', e); }); }, - logoutAndRedirect: function() { - var options = { - postLogoutRedirectUri: window.location.origin - }; - this.oktaAuth.signOut(options) + logoutXHR: async function() { + await this.oktaAuth.revokeAccessToken(); + this.oktaAuth.closeSession() .catch(e => { console.error('Error during signout & redirect: ', e); + }) + .then(() => { + window.location.reload(); }); }, - logoutLocal: function() { - this.clearTokens(); + logoutApp: async function() { + await this.oktaAuth.revokeAccessToken(); + this.oktaAuth.tokenManager.clear(); window.location.reload(); }, handleCallback: async function() { @@ -265,22 +259,19 @@ Object.assign(TestApp.prototype, { this.renderError(e); throw e; }) - .then(tokens => { - this.saveTokens(tokens); - return this.callbackHTML(tokens); + .then(res => { + return this.callbackHTML(res); }) .then(content => this._setContent(content)) .then(() => this._afterRender('callback-handled')); }, getTokensFromUrl: async function() { // parseFromUrl() Will parse the authorization code from the URL fragment and exchange it for tokens - let tokens = await this.oktaAuth.token.parseFromUrl(); - this.saveTokens(tokens); - return tokens; + const res = await this.oktaAuth.token.parseFromUrl(); + this.saveTokens(res.tokens); + return res; }, saveTokens: function(tokens) { - tokens = Array.isArray(tokens) ? tokens : [tokens]; - tokens = tokensArrayToObject(tokens); const { idToken, accessToken } = tokens; if (idToken) { this.oktaAuth.tokenManager.add('idToken', idToken); @@ -321,7 +312,7 @@ Object.assign(TestApp.prototype, { }, appHTML: function(props) { const { idToken, accessToken } = props || {}; - if (idToken && accessToken) { + if (idToken || accessToken) { // Authenticated user home page return ` Welcome back @@ -373,19 +364,21 @@ Object.assign(TestApp.prototype, { `; }, - callbackHTML: function(tokens) { - const success = tokens && tokens.length === 2; + callbackHTML: function(res) { + const tokensReceived = res.tokens ? Object.keys(res.tokens): []; + const success = res.tokens && tokensReceived.length; const errorMessage = success ? '' : 'Tokens not returned. Check error console for more details'; - const successMessage = success ? 'Successfully received tokens on the callback page!' : ''; + const successMessage = success ? 'Successfully received tokens on the callback page: ' + tokensReceived.join(', ') : ''; const content = `
${successMessage}
${errorMessage}
+
State: ${res.state}

${homeLink(this)} - ${ success ? tokensHTML(tokensArrayToObject(tokens)): '' } + ${ success ? tokensHTML(res.tokens): '' } `; return content; } diff --git a/test/app/src/tokens.js b/test/app/src/tokens.js index e52a731e7..4a1bc3adb 100644 --- a/test/app/src/tokens.js +++ b/test/app/src/tokens.js @@ -1,24 +1,8 @@ import { htmlString } from './util'; -function tokensArrayToObject(tokens) { - let accessToken = tokens.filter(token => { - return token.accessToken; - }); - accessToken = accessToken.length ? accessToken[0] : null; - - let idToken = tokens.filter(token => { - return token.idToken; - }); - idToken = idToken.length ? idToken[0] : null; - return { - accessToken, - idToken - }; -} - function tokensHTML(tokens) { const { idToken, accessToken } = tokens; - const claims = idToken.claims; + const claims = idToken ? idToken.claims : {}; const html = ` @@ -38,15 +22,15 @@ function tokensHTML(tokens) {
Access Token
-
${ htmlString(accessToken) }
+
${ accessToken ? htmlString(accessToken) : 'N/A' }
ID Token
-
${ htmlString(idToken) }
+
${ idToken ? htmlString(idToken) : 'N/A' }
`; return html; } -export { tokensArrayToObject, tokensHTML }; +export { tokensHTML }; diff --git a/test/app/src/util.js b/test/app/src/util.js index 37934fa28..f795f8c0b 100644 --- a/test/app/src/util.js +++ b/test/app/src/util.js @@ -20,4 +20,7 @@ function toQueryParams(obj) { } } -export { htmlString, toQueryParams }; +module.exports = { + htmlString, + toQueryParams +}; diff --git a/test/app/src/webpackEntry.js b/test/app/src/webpackEntry.js index cc9d804e2..88d174c8c 100644 --- a/test/app/src/webpackEntry.js +++ b/test/app/src/webpackEntry.js @@ -37,7 +37,7 @@ window.bootstrapLanding = function() { // Callback, read config from storage window.bootstrapCallback = function() { - config = getConfigFromStorage(); + config = getConfigFromStorage() || getDefaultConfig(); clearStorage(); mount(); app.bootstrapCallback(); diff --git a/test/app/webpack.config.js b/test/app/webpack.config.js index a67ca5a18..caae55236 100644 --- a/test/app/webpack.config.js +++ b/test/app/webpack.config.js @@ -1,19 +1,7 @@ /* global process, __dirname */ -const dotenv = require('dotenv'); -const fs = require('fs'); -const path = require('path'); -const ROOT_DIR = path.resolve(__dirname, '..', '..'); - -// Read environment variables from "testenv". Override environment vars if they are already set. -const TESTENV = path.resolve(ROOT_DIR, 'testenv'); - -if (fs.existsSync(TESTENV)) { - const envConfig = dotenv.parse(fs.readFileSync(TESTENV)); - Object.keys(envConfig).forEach((k) => { - process.env[k] = envConfig[k]; - }); -} +require('./env'); // update environment variables from testenv file +const path = require('path'); var webpack = require('webpack'); var PORT = process.env.PORT || 8080; @@ -22,7 +10,8 @@ module.exports = { entry: './src/webpackEntry.js', output: { path: path.join(__dirname, 'public'), - filename: 'oidc-app.js' + filename: 'oidc-app.js', + publicPath: '/' }, plugins: [ new webpack.EnvironmentPlugin(['CLIENT_ID', 'ISSUER']), diff --git a/test/e2e/pageobjects/TestApp.js b/test/e2e/pageobjects/TestApp.js index d330b0951..72c9f7297 100644 --- a/test/e2e/pageobjects/TestApp.js +++ b/test/e2e/pageobjects/TestApp.js @@ -8,9 +8,9 @@ class TestApp { get landingSelector() { return $('body.oidc-app.landing'); } // Authenticated landing - get logoutBtn() { return $('#logout'); } get logoutRedirectBtn() { return $('#logout-redirect'); } - get logoutLocalBtn() { return $('#logout-local'); } + get logoutXHRBtn() { return $('#logout-xhr'); } + get logoutAppBtn() { return $('#logout-app'); } get renewTokenBtn() { return $('#renew-token'); } get revokeTokenBtn() { return $('#revoke-token'); } get getTokenBtn() { return $('#get-token'); } @@ -30,8 +30,9 @@ class TestApp { get password() { return $('#password'); } // Form - get pkceOption() { return $('#pkce'); } get responseModeQuery() { return $('#responseMode [value="query"]'); } + get responseModeFragment() { return $('#responseMode [value="fragment"]'); } + get pkceOption() { return $('#pkce-on'); } get clientId() { return $('#clientId'); } get issuer() { return $('#issuer'); } @@ -103,18 +104,18 @@ class TestApp { await browser.waitUntil(async () => this.landingSelector.then(el => el.isDisplayed())); } - async logout() { - await this.logoutBtn.then(el => el.click()); + async logoutRedirect() { + await this.logoutRedirectBtn.then(el => el.click()); await this.waitForLoginBtn(); } - async logoutLocal() { - await this.logoutLocalBtn.then(el => el.click()); + async logoutXHR() { + await this.logoutXHRBtn.then(el => el.click()); await this.waitForLoginBtn(); } - async logoutRedirect() { - await this.logoutRedirectBtn.then(el => el.click()); + async logoutApp() { + await this.logoutAppBtn.then(el => el.click()); await this.waitForLoginBtn(); } @@ -123,7 +124,7 @@ class TestApp { } async waitForLogoutBtn() { - return browser.waitUntil(async () => this.logoutBtn.then(el => el.isDisplayed()), 15000, 'wait for logout button'); + return browser.waitUntil(async () => this.logoutRedirectBtn.then(el => el.isDisplayed()), 15000, 'wait for logout button'); } async waitForCallback() { diff --git a/test/e2e/pageobjects/TestServer.js b/test/e2e/pageobjects/TestServer.js new file mode 100644 index 000000000..77cad5761 --- /dev/null +++ b/test/e2e/pageobjects/TestServer.js @@ -0,0 +1,53 @@ +import assert from 'assert'; + +/* eslint-disable max-len */ +class TestServer { + get rootSelector() { return $('#root'); } + + // form + get issuer() { return $('#issuer'); } + get username() { return $('#username'); } + get password() { return $('#password'); } + get submitBtn() { return $('#submitBtn'); } + + // results + get status() { return $('#status'); } + get sessionToken() { return $('#sessionToken'); } + get error() { return $('#error'); } + + async open() { + await browser.url('/server'); + await browser.waitUntil(async () => this.rootSelector.then(el => el.isExisting()), 5000, 'wait for root selector'); + } + + async submitLogin() { + await this.submitBtn.then(el => el.click()); + } + + async assertLoginSuccess() { + await this.status.then(el => el.getText()).then(txt => { + assert(txt.trim() === 'SUCCESS'); + }); + await this.error.then(el => el.getText()).then(txt => { + assert(txt.trim() === ''); + }); + await this.sessionToken.then(el => el.getText()).then(txt => { + assert(txt.trim() !== ''); + }); + } + + async assertLoginFailure() { + await this.status.then(el => el.getText()).then(txt => { + assert(txt.trim() === ''); + }); + await this.error.then(el => el.getText()).then(txt => { + assert(txt.trim() === 'AuthApiError: Authentication failed'); + }); + await this.sessionToken.then(el => el.getText()).then(txt => { + assert(txt.trim() === ''); + }); + } + +} + +export default new TestServer(); diff --git a/test/e2e/specs/login.js b/test/e2e/specs/login.js index 5865c1bd4..0a7361b1a 100644 --- a/test/e2e/specs/login.js +++ b/test/e2e/specs/login.js @@ -6,15 +6,15 @@ import { loginRedirect, loginPopup, loginDirect } from '../util/loginUtils'; describe('E2E login', () => { // responseMode=query is not supported for implicit flow - it('can login using redirect (responseMode=query)', async () => { - await openPKCE({ responseMode: 'query' }); - await TestApp.responseModeQuery.then(el => el.isSelected()).then(isSelected => { + it('PKCE: can login using redirect with responseMode=fragment', async () => { + await openPKCE({ responseMode: 'fragment' }); + await TestApp.responseModeFragment.then(el => el.isSelected()).then(isSelected => { assert(isSelected === true); }); - await loginRedirect('pkce', 'query'); + await loginRedirect('pkce', 'fragment'); await TestApp.getUserInfo(); await TestApp.assertUserInfo(); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); flows.forEach(flow => { @@ -27,21 +27,21 @@ describe('E2E login', () => { await loginRedirect(flow); await TestApp.getUserInfo(); await TestApp.assertUserInfo(); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); it('can login using a popup window', async() => { await loginPopup(flow); await TestApp.getUserInfo(); await TestApp.assertUserInfo(); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); it('can login directly, calling signin() with username and password', async () => { await loginDirect(flow); await TestApp.getUserInfo(); await TestApp.assertUserInfo(); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); }); }); diff --git a/test/e2e/specs/logout.js b/test/e2e/specs/logout.js index 641bb4202..1fa5dd028 100644 --- a/test/e2e/specs/logout.js +++ b/test/e2e/specs/logout.js @@ -11,41 +11,63 @@ describe('E2E logout', () => { await loginPopup(); }); - it('can logout locally, keeping remote user session open', async () => { - await TestApp.logoutLocal(); - - // We should still be logged into Okta - await openOktaHome(); - await OktaHome.waitForLoad(); - - // Now sign the user out - await OktaHome.closeInitialPopUp(); - await OktaHome.signOut(); - await browser.closeWindow(); - await switchToMainWindow(); - + describe('logoutApp', () => { + it('can clear app session, keeping remote SSO session open', async () => { + await TestApp.logoutApp(); + + // We should still be logged into Okta + await openOktaHome(); + await OktaHome.waitForLoad(); + + // Now sign the user out + await OktaHome.closeInitialPopUp(); + await OktaHome.signOut(); + await browser.closeWindow(); + await switchToMainWindow(); + }); + }); - it('can logout from okta, ending remote user session', async() => { - await TestApp.logout(); - - // We should not be logged into Okta - await openOktaHome(); - await OktaLogin.waitForLoad(); - - // cleanup - await browser.closeWindow(); - await switchToMainWindow(); - }); + describe('logoutRedirect', () => { + it('can logout from okta, ending remote user session', async() => { + await TestApp.assertIdToken(); + await TestApp.logoutRedirect(); + + // We should not be logged into Okta + await openOktaHome(); + await OktaLogin.waitForLoad(); + + // cleanup + await browser.closeWindow(); + await switchToMainWindow(); + }); + + it('no idToken: can logout from okta (using XHR fallback) and end back to the application', async () => { + await TestApp.clearTokens(); + await TestApp.logoutRedirect(); - it('can logout from okta and redirect back to the application', async () => { - await TestApp.assertIdToken(); - await TestApp.logoutRedirect(); + // We should not be logged into Okta + await openOktaHome(); + await OktaLogin.waitForLoad(); + + // cleanup + await browser.closeWindow(); + await switchToMainWindow(); + }); }); - it('no idToken: can logout from okta and redirect back to the application', async () => { - await TestApp.clearTokens(); - await TestApp.logoutRedirect(); + describe('logoutXHR', () => { + it('can logout from okta using XHR, ending remote user session', async() => { + await TestApp.logoutXHR(); + + // We should not be logged into Okta + await openOktaHome(); + await OktaLogin.waitForLoad(); + + // cleanup + await browser.closeWindow(); + await switchToMainWindow(); + }); }); }); diff --git a/test/e2e/specs/server.js b/test/e2e/specs/server.js new file mode 100644 index 000000000..ec6ef9f43 --- /dev/null +++ b/test/e2e/specs/server.js @@ -0,0 +1,31 @@ +import TestServer from '../pageobjects/TestServer'; + +const ISSUER = process.env.ISSUER; +const USERNAME = process.env.USERNAME; +const PASSWORD = process.env.PASSWORD; + +describe('Server-side login', () => { + + beforeEach(async () => { + await TestServer.open(); + }); + + it('can receive sessionToken with valid username/password', async () => { + await TestServer.issuer.then(el => el.setValue(ISSUER)); + await TestServer.username.then(el => el.setValue(USERNAME)); + await TestServer.password.then(el => el.setValue(PASSWORD)); + + await TestServer.submitLogin(); + await TestServer.assertLoginSuccess(); + }); + + it('will throw error for wrong password', async() => { + await TestServer.issuer.then(el => el.setValue(ISSUER)); + await TestServer.username.then(el => el.setValue(USERNAME)); + await TestServer.password.then(el => el.setValue('wrong password!!!')); + + await TestServer.submitLogin(); + await TestServer.assertLoginFailure(); + }); + +}); \ No newline at end of file diff --git a/test/e2e/specs/tokens.js b/test/e2e/specs/tokens.js index 04445d99c..723331d74 100644 --- a/test/e2e/specs/tokens.js +++ b/test/e2e/specs/tokens.js @@ -31,7 +31,7 @@ describe('token revoke', () => { const xhrError = await TestApp.xhrError.then(el => el.getText()); assert(xhrError === 'Network request failed'); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); }); @@ -52,7 +52,7 @@ describe('E2E token flows', () => { return txt !== prevToken; }, 10000); await TestApp.assertLoggedIn(); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); it('can refresh all tokens', async () => { @@ -71,7 +71,7 @@ describe('E2E token flows', () => { ); }, 10000); await TestApp.assertLoggedIn(); - await TestApp.logout(); + await TestApp.logoutRedirect(); }); it('Can receive an error on token renew if user has signed out from Okta page', async () => { @@ -93,6 +93,7 @@ describe('E2E token flows', () => { await TestApp.sessionExpired.then(el => el.getText()).then(txt => { assert(txt === 'SESSION EXPIRED'); }); + await TestApp.clearTokens(); await browser.refresh(); await TestApp.waitForLoginBtn(); // assert we are logged out }); diff --git a/test/e2e/util/appUtils.js b/test/e2e/util/appUtils.js index 95b9fce27..22a66782a 100644 --- a/test/e2e/util/appUtils.js +++ b/test/e2e/util/appUtils.js @@ -7,7 +7,7 @@ const CLIENT_ID = process.env.CLIENT_ID; const flows = ['implicit', 'pkce']; async function openImplicit(options) { - await TestApp.open(Object.assign({ issuer: ISSUER, clientId: CLIENT_ID }, options)); + await TestApp.open(Object.assign({ issuer: ISSUER, clientId: CLIENT_ID, pkce: false }, options)); await TestApp.pkceOption.then(el => el.isSelected()).then(isSelected => { assert(isSelected === false); }); diff --git a/test/e2e/util/loginUtils.js b/test/e2e/util/loginUtils.js index 7b51ac635..64ceff985 100644 --- a/test/e2e/util/loginUtils.js +++ b/test/e2e/util/loginUtils.js @@ -7,7 +7,7 @@ const USERNAME = process.env.USERNAME; const PASSWORD = process.env.PASSWORD; function assertPKCE(url, responseMode) { - const char = responseMode === 'query' ? '?' : '#'; + const char = responseMode === 'fragment' ? '#' : '?'; const str = url.split(char)[1]; assert(str.indexOf('code' > 0)); } diff --git a/test/support/factory.js b/test/support/factory.js index b994a919a..a061e4913 100644 --- a/test/support/factory.js +++ b/test/support/factory.js @@ -1,3 +1,4 @@ +/* global btoa */ var factory = {}; // converts a standard base64-encoded string to a "url/filename safe" variant diff --git a/test/support/oauthUtil.js b/test/support/oauthUtil.js index 5e1a9b800..4737eb22c 100644 --- a/test/support/oauthUtil.js +++ b/test/support/oauthUtil.js @@ -1,9 +1,9 @@ +/* global window, document, Storage, localStorage */ /* eslint-disable complexity, max-statements */ var URL = require('url').URL; var util = require('./util'); var OktaAuth = require('OktaAuth'); var tokens = require('./tokens'); -var Q = require('q'); var EventEmitter = require('tiny-emitter'); var wellKnown = require('./xhr/well-known'); var wellKnownSharedResource = require('./xhr/well-known-shared-resource'); @@ -76,14 +76,25 @@ oauthUtil.loadWellKnownAndKeysCache = function() { var defaultPostMessage = oauthUtil.defaultPostMessage = { 'id_token': tokens.standardIdToken, + 'access_token': tokens.standardAccessToken, state: oauthUtil.mockedState }; var defaultResponse = { - idToken: tokens.standardIdToken, - claims: tokens.standardIdTokenClaims, - expiresAt: 1449699930, - scopes: ['openid', 'email'] + state: oauthUtil.mockedState, + tokens: { + accessToken: { + value: tokens.standardAccessToken, + accessToken: tokens.standardAccessToken + }, + idToken: { + value: tokens.standardIdToken, + idToken: tokens.standardIdToken, + claims: tokens.standardIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'email'] + } + } }; var getTime = oauthUtil.getTime = function getTime(time) { @@ -100,6 +111,7 @@ function validateResponse(res, expectedResp) { expect(actual, expected); return; } + expect(actual.value).toEqual(expected.value); expect(actual.idToken).toEqual(expected.idToken); expect(actual.claims).toEqual(expected.claims); expect(actual.accessToken).toEqual(expected.accessToken); @@ -160,7 +172,8 @@ oauthUtil.setup = function(opts) { authClient = opts.authClient; } else { authClient = new OktaAuth({ - url: 'https://auth-js-test.okta.com' + pkce: false, + issuer: 'https://auth-js-test.okta.com' }); } @@ -199,14 +212,15 @@ oauthUtil.setup = function(opts) { } else if (opts.tokenRenewArgs) { promise = authClient.token.renew.apply(this, opts.tokenRenewArgs); } else if (opts.autoRenew) { - var renewDeferred = Q.defer(); - authClient.tokenManager.on('renewed', function() { - renewDeferred.resolve(); - }); - authClient.tokenManager.on('error', function() { - renewDeferred.resolve(); + promise = new Promise(function(resolve) { + // TODO: do we need to remove handlers? + authClient.tokenManager.on('renewed', function() { + resolve(); + }); + authClient.tokenManager.on('error', function() { + resolve(); + }); }); - promise = renewDeferred.promise; } if (opts.fastForwardToTime) { @@ -228,7 +242,7 @@ oauthUtil.setup = function(opts) { var expectedResp = opts.expectedResp || defaultResponse; validateResponse(res, expectedResp); }) - .fail(function(err) { + .catch(function(err) { if (opts.willFail) { throw err; } else { @@ -349,7 +363,8 @@ oauthUtil.setupPopup = function(opts) { oauthUtil.setupRedirect = function(opts) { var client = new OktaAuth(Object.assign({ - url: 'https://auth-js-test.okta.com', + pkce: false, + issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' }, opts.oktaAuthArgs)); @@ -376,11 +391,12 @@ oauthUtil.setupRedirect = function(opts) { }; oauthUtil.setupParseUrl = function(opts) { - var client = new OktaAuth(opts.oktaAuthArgs || { - url: 'https://auth-js-test.okta.com', + var client = new OktaAuth(Object.assign({ + pkce: false, + issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' - }); + }, opts.oktaAuthArgs)); // Mock the well-known and keys request oauthUtil.loadWellKnownAndKeysCache(); @@ -450,7 +466,8 @@ oauthUtil.setupParseUrl = function(opts) { oauthUtil.setupSimultaneousPostMessage = function() { // Create client var client = new OktaAuth({ - url: 'https://auth-js-test.okta.com', + pkce: false, + issuer: 'https://auth-js-test.okta.com', clientId: 'NPSfOkH5eZrTy8PMDlvx', redirectUri: 'https://example.com/redirect' }); @@ -488,7 +505,7 @@ oauthUtil.setupSimultaneousPostMessage = function() { // warp to time to ensure tokens aren't expired util.warpToUnixTime(tokens.standardIdTokenClaims.exp - 1); - return new Q({ + return Promise.resolve({ client: client, emitter: emitter }); @@ -510,7 +527,7 @@ oauthUtil.itpErrorsCorrectly = function(title, options, error) { .then(function() { expect('not to be hit').toEqual(true); }) - .fail(function(e) { + .catch(function(e) { util.expectErrorToEqual(e, error); }); }); diff --git a/test/support/tokens.js b/test/support/tokens.js index 15b9622f1..bc0d95338 100644 --- a/test/support/tokens.js +++ b/test/support/tokens.js @@ -60,6 +60,7 @@ tokens.standardIdTokenClaims = { }; tokens.standardIdTokenParsed = { + value: tokens.standardIdToken, idToken: tokens.standardIdToken, claims: tokens.standardIdTokenClaims, expiresAt: 1449699930, @@ -105,6 +106,7 @@ tokens.standardIdToken2Claims = { }; tokens.standardIdToken2Parsed = { + value: tokens.standardIdToken2, idToken: tokens.standardIdToken2, claims: tokens.standardIdToken2Claims, expiresAt: 1449699930, @@ -149,6 +151,7 @@ tokens.expiredBeforeIssuedIdTokenClaims = { }; tokens.expiredBeforeIssuedIdTokenParsed = { + value: tokens.expiredBeforeIssuedIdToken, idToken: tokens.expiredBeforeIssuedIdToken, claims: tokens.expiredBeforeIssuedIdTokenClaims, expiresAt: 1449690000, @@ -186,6 +189,7 @@ tokens.authServerIdTokenClaims = { }; tokens.authServerIdTokenParsed = { + value: tokens.authServerIdToken, idToken: tokens.authServerIdToken, claims: tokens.authServerIdTokenClaims, expiresAt: 1449699930, @@ -244,6 +248,7 @@ tokens.standardAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOj' + 'EQ9-Ua9rPOMaO0pFC6h2lfB_HfzGifXATKsN-wLdxk6cgA'; tokens.standardAccessTokenParsed = { + value: tokens.standardAccessToken, accessToken: tokens.standardAccessToken, expiresAt: 1449703529, // assuming time = 1449699929 scopes: ['openid', 'email'], @@ -266,6 +271,7 @@ tokens.authServerAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOjE 'h9gY9Z3xd92ac407ZIOHkabLvZ0-45ANM3Gm0LC0c'; tokens.authServerAccessTokenParsed = { + value: tokens.authServerAccessToken, accessToken: tokens.authServerAccessToken, expiresAt: 1449703529, // assuming time = 1449699929 scopes: ['openid', 'email'], diff --git a/test/support/util.js b/test/support/util.js index 0f3f6e308..423f43669 100644 --- a/test/support/util.js +++ b/test/support/util.js @@ -1,8 +1,7 @@ /* globals expect, JSON */ /* eslint-disable max-statements, complexity */ -var Q = require('q'), - _ = require('lodash'), +var _ = require('lodash'), OktaAuth = require('OktaAuth'), cookies = require('@okta/okta-auth-js/lib/browser/browserStorage').storage, fetch = require('cross-fetch'); @@ -34,12 +33,12 @@ util.warpByTicksToUnixTime = function (unixTime) { }; function generateXHRPair(request, response, uri, responseVars) { - return Q.Promise(function(resolve) { + return new Promise(function(resolve) { responseVars = responseVars || {}; responseVars.uri = responseVars.uri || uri; // Import the desired xhr - var responseXHR = require('./xhr/' + response); + var responseXHR = typeof response === 'object' ? response : require('./xhr/' + response); // Change the request uri to include the domain if (request) { @@ -105,27 +104,25 @@ function mockAjax(pairs) { } } - var deferred = Q.defer(); - var xhr = pair.response; - xhr.headers = xhr.headers || {}; - xhr.headers['Content-Type'] = 'application/json'; - xhr.headers.get = function(attr) { - return xhr.headers[attr]; - }; - xhr.ok = xhr.status >= 200 && xhr.status < 300; - xhr.json = function() { - return Q.Promise(function(resolve) { - resolve(xhr.responseText); - }); - }; - - if (xhr.status > 0 && xhr.status < 300) { - _.defer(function () { deferred.resolve(xhr); }); - } else { - xhr.responseJSON = xhr.response; - deferred.reject(xhr); - } - return deferred.promise; + return new Promise(function(resolve, reject) { + var xhr = pair.response; + xhr.headers = xhr.headers || {}; + xhr.headers['Content-Type'] = 'application/json'; + xhr.headers.get = function(attr) { + return xhr.headers[attr]; + }; + xhr.ok = xhr.status >= 200 && xhr.status < 300; + xhr.json = function() { + return Promise.resolve(xhr.responseText); + }; + + if (xhr.status > 0 && xhr.status < 300) { + _.defer(function () { resolve(xhr); }); + } else { + xhr.responseJSON = xhr.response; + reject(xhr); + } + }); }); return { @@ -135,13 +132,22 @@ function mockAjax(pairs) { } function setup(options) { - if (!options.uri) { - options.uri = 'https://auth-js-test.okta.com'; + if (typeof options === 'function') { + options = options(); + } + + if (!options.issuer) { + options.issuer = 'https://auth-js-test.okta.com'; + } + var baseUri = options.issuer.indexOf('/oauth2') > 0 ? options.issuer.split('/oauth2')[0] : options.issuer; + + if (typeof options.pkce === 'undefined') { + options.pkce = false; } var ajaxMock, resReply, oa, trans; - return new Q() + return Promise.resolve() .then(function() { if (options.time) { @@ -154,18 +160,18 @@ function setup(options) { // Get all the pairs and load the mock var xhrGenPromises = []; _.each(options.calls, function(call) { - var xhrGenPromise = generateXHRPair(call.request, call.response, options.uri, call.responseVars); + var xhrGenPromise = generateXHRPair(call.request, call.response, baseUri, call.responseVars); xhrGenPromises.push(xhrGenPromise); }); - return Q.all(xhrGenPromises) + return Promise.all(xhrGenPromises) .then(function (pairs) { ajaxMock = mockAjax(pairs); resReply = _.last(pairs).response; }); } else if (options.response) { - return generateXHRPair(options.request, options.response, options.uri, options.responseVars) + return generateXHRPair(options.request, options.response, baseUri, options.responseVars) .then(function(pair) { // Load the single response as a pair ajaxMock = mockAjax(pair); @@ -182,7 +188,7 @@ function setup(options) { // 2. Setup OktaAuth oa = new OktaAuth({ - url: options.uri, + pkce: options.pkce, issuer: options.issuer, transformErrorXHR: options.transformErrorXHR, headers: options.headers, @@ -197,7 +203,7 @@ function setup(options) { stateToken: 'dummy' } }; - return generateXHRPair(request, options.status, options.uri) + return generateXHRPair(request, options.status, baseUri) .then(function(pair) { ajaxMock.setNextPair({ request: pair.request, @@ -232,8 +238,6 @@ util.itMakesCorrectRequestResponse = function (options) { fn(title, function () { return setup(options.setup).then(function (test) { return options.execute(test) - // Add a tick for the setTimeout successFn - .delay(0) .then(function (res) { if (res.data) { test.trans = res; @@ -254,12 +258,7 @@ util.itErrorsCorrectly = function (options) { fn(options.title, function (done) { return setup(options.setup).then(function (test) { return options.execute(test) - // Add a tick for the setTimeout successFn - .then(null, function(err) { - return Q.delay(0) - .thenReject(err); - }) - .fail(function (err) { + .catch(function (err) { if (options.expectations) { options.expectations(test, err); test.ajaxMock.done(); @@ -289,7 +288,7 @@ util.itErrorChecksInput = function (options) { fn(options.title, function (done) { return setup(options.setup).then(function (test) { return options.execute(test) - .fail(function (err) { + .catch(function (err) { util.assertAuthSdkError(err, options.errorMsg); test.ajaxMock.done(); done(); @@ -338,7 +337,7 @@ util.mockDeleteCookie = function () { }; util.mockGetCookie = function (text) { - jest.spyOn(cookies, 'get').mockReturnValue(text || ''); + return jest.spyOn(cookies, 'get').mockReturnValue(text || ''); }; util.mockGetHistory = function (client, mockHistory) { diff --git a/yarn.lock b/yarn.lock index 9a8f84a7c..dcb27d487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,22 @@ # yarn lockfile v1 +"@babel/cli@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.8.3.tgz#121beb7c273e0521eb2feeb3883a2b7435d12328" + integrity sha512-K2UXPZCKMv7KwWy9Bl4sa6+jTNP7JyDiHKzoOiUUygaEDbC60vaargZDnO9oFMvlq8pIKOOyUUgeMYrsaN9djA== + dependencies: + commander "^4.0.1" + convert-source-map "^1.1.0" + fs-readdir-recursive "^1.1.0" + glob "^7.0.0" + lodash "^4.17.13" + make-dir "^2.1.0" + slash "^2.0.0" + source-map "^0.5.0" + optionalDependencies: + chokidar "^2.1.8" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -18,7 +34,7 @@ invariant "^2.2.4" semver "^5.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.6.4": +"@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.6.4", "@babel/core@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941" integrity sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA== @@ -609,6 +625,16 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-runtime@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.3.tgz#c0153bc0a5375ebc1f1591cb7eea223adea9f169" + integrity sha512-/vqUt5Yh+cgPZXXjmaG9NT8aVfThKk7G4OqkVhrXqwsC5soMn/qTCxs36rZ2QFhpfTJcjw4SNDIZ4RUb8OL4jQ== + dependencies: + "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + resolve "^1.8.1" + semver "^5.5.1" + "@babel/plugin-transform-shorthand-properties@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" @@ -663,7 +689,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/preset-env@^7.1.6", "@babel/preset-env@^7.6.3": +"@babel/preset-env@^7.1.6", "@babel/preset-env@^7.6.3", "@babel/preset-env@^7.8.2": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.3.tgz#dc0fb2938f52bbddd79b3c861a4b3427dd3a6c54" integrity sha512-Rs4RPL2KjSLSE2mWAx5/iCH+GC1ikKdxPrhnRS6PfFVaiZeom22VFKN4X8ZthyN61kAaR05tfXTbCvatl9WIQg== @@ -753,6 +779,13 @@ pirates "^4.0.0" source-map-support "^0.5.16" +"@babel/runtime@^7.7.7": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1" + integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.4.0", "@babel/template@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8" @@ -1088,9 +1121,9 @@ integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== "@types/istanbul-lib-report@*": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" - integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: "@types/istanbul-lib-coverage" "*" @@ -1108,9 +1141,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "13.1.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.6.tgz#076028d0b0400be8105b89a0a55550c86684ffec" - integrity sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg== + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b" + integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1128,20 +1161,20 @@ integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== "@types/yargs@^13.0.0": - version "13.0.5" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.5.tgz#18121bfd39dc12f280cee58f92c5b21d32041908" - integrity sha512-CF/+sxTO7FOwbIRL4wMv0ZYLCRfMid2HQpzDRyViH7kSpfoAFiMdGqKIxb1PxWfjtQXQhnQuD33lvRHNwr809Q== + version "13.0.6" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.6.tgz#6aed913a92c262c13b94d4bca8043237de202124" + integrity sha512-IkltIncDQWv6fcAvnHtJ6KtkmY/vtR3bViOaCzpj/A3yNhlfZAgxNe6AEQD1cQrkYD+YsKVo08DSxvNKEsD7BA== dependencies: "@types/yargs-parser" "*" "@wdio/cli@^5.15.1": - version "5.18.5" - resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-5.18.5.tgz#549ffd1813338de80b25544f27937f4291166816" - integrity sha512-CLp9yO9jjjzb+zFJnkE+r2qt8ozmf4ed4na7nXmCl9xaNYlXNanUDVtBODW9aSjuJfQDohW9W47emk+RruwbdA== + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-5.18.6.tgz#a1052e0e22cd35a23d7f42202a8ad02eefbe5729" + integrity sha512-9znD/Po9402+LXXe/0P00AHUDWIJVetf4uZtua9yMp9w+PyIDd+b0gF4jfuVsbITt/EFQBU615DVtJNQm/9ahw== dependencies: "@wdio/config" "5.18.4" "@wdio/logger" "5.16.10" - "@wdio/utils" "5.16.15" + "@wdio/utils" "5.18.6" async-exit-hook "^2.0.1" chalk "^3.0.0" chokidar "^3.0.0" @@ -1153,7 +1186,7 @@ lodash.pickby "^4.6.0" lodash.union "^4.6.0" log-update "^3.2.0" - webdriverio "5.18.5" + webdriverio "5.18.6" yargs "^15.0.1" yarn-install "^1.0.0" @@ -1167,13 +1200,13 @@ glob "^7.1.2" "@wdio/local-runner@^5.15.1": - version "5.18.5" - resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-5.18.5.tgz#ffedeb68d6c76880996f5e875c6b2e0bc51657f6" - integrity sha512-S84vhSzB4a7DhVpu+aTHsFYy2MYk0FYcLgJfCNfPnJSw7MVm/QBp3J6UyzRl6SpQP6suV3TKAQ/JfIWwPKRKdA== + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-5.18.6.tgz#9d6a07ccdcc171f4838d2e15e3fe5189a7c10271" + integrity sha512-re7K5nPOLi8RiZiMbujYwGOrEvlbTi/0bEwu1f2Ms+mtG+efCxJ6+ng+ZMFJ2KNmdxZu+bsGbGEjI9vJ8KPYEA== dependencies: "@wdio/logger" "5.16.10" - "@wdio/repl" "5.16.15" - "@wdio/runner" "5.18.5" + "@wdio/repl" "5.18.6" + "@wdio/runner" "5.18.6" async-exit-hook "^2.0.1" stream-buffers "^3.0.2" @@ -1188,12 +1221,12 @@ strip-ansi "^6.0.0" "@wdio/mocha-framework@^5.15.1": - version "5.16.15" - resolved "https://registry.yarnpkg.com/@wdio/mocha-framework/-/mocha-framework-5.16.15.tgz#c464eef3561444d2fde658887e8b859c622f4230" - integrity sha512-nWwpghEAgQLh2NDP0dBcqPHONk56MLsAWPfHhOiT1coWsyvKQ60r9jD/8i/iQz+i9hldlAI5Or0pKQx9fTjkBA== + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/mocha-framework/-/mocha-framework-5.18.6.tgz#70d63656c7a8dbbb07d443ae303408219d2c2729" + integrity sha512-wmm5uMII3cpLp1bIc1zWWqkKBrnu3m20byM9D9pFQxWusu5/OBgmsrsL2uDkRpm1+vLjjVoJWa0jnZRE2JRekw== dependencies: "@wdio/logger" "5.16.10" - "@wdio/utils" "5.16.15" + "@wdio/utils" "5.18.6" mocha "^6.1.0" "@wdio/protocols@5.16.7": @@ -1201,46 +1234,46 @@ resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-5.16.7.tgz#cf8af4fe9e362e879e9b39543ec8ad1770847764" integrity sha512-fBlK/lfKeyxTQOfzgnpE0F7tBwb9maTgVkqv2LlvameB0Rsy18js1/cUhjJkpESXiK9md0si/CROdNaNliBKmg== -"@wdio/repl@5.16.15": - version "5.16.15" - resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-5.16.15.tgz#b419e4ee4079386bd1baebbbf2359f636ff901ac" - integrity sha512-AklEbnjL7uxr5+7IgD1RZEE1c7QKQpmNKx8DrOPVjNJIBjUmcatOFG+9R+sysyRI957DA5tlnU513dtF5rlGZg== +"@wdio/repl@5.18.6": + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-5.18.6.tgz#6e3467ff6e52879f726579a2053baf98a00e9a0f" + integrity sha512-z9UPBk/Uee0l9g0ijnOatU3WP7TcpIyNtRj9AGsJVbYZFwqMWBqPkO4nblldyNQIuqdgXAPsDo8lPGDno12/oA== dependencies: - "@wdio/utils" "5.16.15" + "@wdio/utils" "5.18.6" -"@wdio/reporter@5.15.2": - version "5.15.2" - resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-5.15.2.tgz#578a2f99aaa02c5d419230a6b18d759960504478" - integrity sha512-M2eBZDvJIfa8ao81hcNbl9tN/pDtflSFFBa9iDcwPdztVplI50ob4xTlrsztv8V3Ox2En1TnfX1bQiV7E7gTiA== +"@wdio/reporter@5.18.6": + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-5.18.6.tgz#00b0a565fc23abc5be9295ef18f42c30fa476f7b" + integrity sha512-Q9fGfH0XqvRPuDSmfvsfaR0L7varPGfrzELebpx6P3jx4Xqp2AZTSewEWkmn04svPFo0CIjV/+IcKjlJbk13NQ== dependencies: fs-extra "^8.0.1" -"@wdio/runner@5.18.5": - version "5.18.5" - resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-5.18.5.tgz#7b285e39b052905afb3dba64aab5fa6d3531ffea" - integrity sha512-jq9DMJw8s0cod6ioAPr9bpuDqRmr4MhixCLjFnNVntIK8x2pJSSMwraWMR1D8lmRWuqrPE9TWCUGydOWUjAr/g== +"@wdio/runner@5.18.6": + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-5.18.6.tgz#ec6f24705ee8ecbae001a0a0b440a3764c9bd790" + integrity sha512-d/Z1w9p69KYq8TeDEa5+/8YE+Ip1TXFXBB9jW2IvmBDnSa08eBn/OQSeOhiVQlEDDEqxENMjwa/OMYRCTzVsEg== dependencies: "@wdio/config" "5.18.4" "@wdio/logger" "5.16.10" - "@wdio/utils" "5.16.15" + "@wdio/utils" "5.18.6" deepmerge "^4.0.0" gaze "^1.1.2" - webdriverio "5.18.5" + webdriverio "5.18.6" "@wdio/spec-reporter@^5.15.1": - version "5.16.11" - resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-5.16.11.tgz#5eaaef37a02f076bf1ebb4452d5e7dad0db558ca" - integrity sha512-UFqgq2VRgFxGBeIPufITKFZdrw6IuT/PN3uAoxWwoazicy0ghcOOwtVQpXsi0MIC6T3GsWIPdRgVhzk73l1U8w== + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-5.18.6.tgz#9951d7a14688a67cd2d7dba8a79f30852307683d" + integrity sha512-W0+igtkBAkcXmoVEAiMoZUx2RSMCSO1R4yzxwr4ZOBlqeT7gp58uUJyzfiRdxjFK6Moll78Xujq6C+jXDzVH2w== dependencies: - "@wdio/reporter" "5.15.2" + "@wdio/reporter" "5.18.6" chalk "^3.0.0" easy-table "^1.1.1" pretty-ms "^5.0.0" -"@wdio/utils@5.16.15": - version "5.16.15" - resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-5.16.15.tgz#c2228dcb196079a2e6c8758506de05d5ba3196e2" - integrity sha512-xeZSAeDtzm1+zVRjTuhsEKpcXokOZU1NrnXBacNNwyKIfRMwPUmyOX3WvKtHL+2OuLXjKP9dlRMHbPgX/nsz4w== +"@wdio/utils@5.18.6": + version "5.18.6" + resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-5.18.6.tgz#f46c91a3f5dda5cf20cc4dae2f15b336e0834dc3" + integrity sha512-OVdK7P9Gne9tR6dl1GEKucwX4mtS47F26g4lH8r0HURvMegZLGtcchI1cqF6hjK7EpP737b+C3q4ooZSBdH9XQ== dependencies: "@wdio/logger" "5.16.10" deepmerge "^4.0.0" @@ -1542,11 +1575,11 @@ ajv@^5.0.0: json-schema-traverse "^0.3.0" ajv@^6.0.1, ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.3, ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== dependencies: - fast-deep-equal "^2.0.1" + fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" @@ -1825,6 +1858,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +ast-metadata-inferer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ast-metadata-inferer/-/ast-metadata-inferer-0.1.1.tgz#66e24fae9d30ca961fac4880b7fc466f09b25165" + integrity sha512-hc9w8Qrgg9Lf9iFcZVhNjUnhrd2BBpTlyCnegPVvCe6O0yMrF57a6Cmh7k+xUsfUOMh9wajOL5AsGOBNEyTCcw== + ast-types@0.11.7: version "0.11.7" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.7.tgz#f318bf44e339db6a320be0009ded64ec1471f46c" @@ -1930,6 +1968,16 @@ babel-jest@^24.9.0: chalk "^2.4.2" slash "^2.0.0" +babel-loader@^8.0.6: + version "8.0.6" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" + integrity sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw== + dependencies: + find-cache-dir "^2.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + pify "^4.0.1" + babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" @@ -2265,13 +2313,13 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.8.2, browserslist@^4.8.3: - version "4.8.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.3.tgz#65802fcd77177c878e015f0e3189f2c4f627ba44" - integrity sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg== + version "4.8.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.5.tgz#691af4e327ac877b25e7a3f7ee869c4ef36cdea3" + integrity sha512-4LMHuicxkabIB+n9874jZX/az1IaZ5a+EUuvD7KFOu9x/Bd5YHyO0DIz2ls/Kl8g0ItS4X/ilEgf4T1Br0lgSg== dependencies: - caniuse-lite "^1.0.30001017" - electron-to-chromium "^1.3.322" - node-releases "^1.1.44" + caniuse-lite "^1.0.30001022" + electron-to-chromium "^1.3.338" + node-releases "^1.1.46" bser@2.1.1: version "2.1.1" @@ -2494,10 +2542,15 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001017: - version "1.0.30001020" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001020.tgz#3f04c1737500ffda78be9beb0b5c1e2070e15926" - integrity sha512-yWIvwA68wRHKanAVS1GjN8vajAv7MBFshullKCeq/eKpK7pJBVDgFFEqvgWTkcP2+wIDeQGYFRXECjKZnLkUjA== +caniuse-db@^1.0.30001017: + version "1.0.30001022" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001022.tgz#a7721c26a4af4d8420680079dcd27754be84daf6" + integrity sha512-2RQQgO+yDEaqF4ltwrCja7oZst+FVnXHQLSJgZ678tausEljBq3/U20Fedvze+Hxqm8XLV+9OgGbtdgS7ksnRw== + +caniuse-lite@^1.0.30001022: + version "1.0.30001022" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6" + integrity sha512-FjwPPtt/I07KyLPkBQ0g7/XuZg6oUkYBVnPHNj3VHJbOjmmJ/GdSo/GUY6MwINEQvjhP6WZVbX8Tvms8xh0D5A== capture-exit@^2.0.0: version "2.0.0" @@ -2862,6 +2915,11 @@ commander@^2.20.0, commander@~2.20.3: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83" + integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3154,7 +3212,7 @@ conventional-recommended-bump@^1.2.1: meow "^3.3.0" object-assign "^4.0.1" -convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0: +convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -3804,10 +3862,10 @@ ejs@^3.0.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.1.tgz#30c8f6ee9948502cc32e85c37a3f8b39b5a614a5" integrity sha512-cuIMtJwxvzumSAkqaaoGY/L6Fc/t6YvoP9/VIaK0V/CyqKLEQ8sqODmYfy/cjXEdZ9+OOL8TecbJu+1RsofGDw== -electron-to-chromium@^1.3.322: - version "1.3.334" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.334.tgz#0588359f4ac5c4185ebacdf5fc7e1937e2c99872" - integrity sha512-RcjJhpsVaX0X6ntu/WSBlW9HE9pnCgXS9B8mTUObl1aDxaiOa0Lu+NMveIS5IDC+VELzhM32rFJDCC+AApVwcA== +electron-to-chromium@^1.3.338: + version "1.3.339" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.339.tgz#ff7b56c4bc58159f0d6623591116e4414e7a618b" + integrity sha512-C1i/vH6/kQx9YV8RddMkmW216GwW4pTrnYIlKmDFIqXA4fPwqDxIdGyHsuG+fgurHoljRz7/oaD+tztcryW/9g== elliptic@^6.0.0: version "6.5.2" @@ -3948,10 +4006,10 @@ error@^7.0.2: dependencies: string-template "~0.2.1" -es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1: - version "1.17.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.1.tgz#1331afa4cba2628b63e988104b9846c2d631b380" - integrity sha512-WmWNHWmm/LDwK8jaeZic/g6sU1ZckM+vvOyCV1qFRhJJ6hzve6DRgthNQB7Lra1ocrw68HexLKYgtdxIPcb3Fg== +es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" @@ -3970,7 +4028,7 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-get-iterator@^1.0.1: +es-get-iterator@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.0.2.tgz#bc99065aa8c98ce52bc86ab282dedbba4120e0b3" integrity sha512-ZHb4fuNK3HKHEOvDGyHPKf5cSWh/OvAMskeM/+21NMnTuvqFvz8uHatolu+7Kf6b6oK9C+3Uo1T37pSGPWv0MA== @@ -4070,11 +4128,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.9.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.1.tgz#08770602a74ac34c7a90ca9229e7d51e379abc76" - integrity sha512-Q8t2YZ+0e0pc7NRVj3B4tSQ9rim1oi4Fh46k2xhJ2qOiEwhQfdjyEQddWdj7ZFaKmU+5104vn1qrcjEPWq+bgQ== + version "1.13.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.13.0.tgz#c7adf9bd3f3cc675bb752f202f79a720189cab29" + integrity sha512-eYk2dCkxR07DsHA/X2hRBj0CFAZeri/LyDMc0C8JT1Hqi6JnVpMhJ7XFITbb0+yZS3lVkaPL2oCkZ3AVmeVbMw== dependencies: - esprima "^3.1.3" + esprima "^4.0.1" estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" @@ -4091,6 +4149,19 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-plugin-compat@^3.3.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-3.5.1.tgz#09f9c05dcfa9b5cd69345d7ab333749813ed8b14" + integrity sha512-dhfW12vZxxKLEVhrPoblmEopgwpYU2Sd4GdXj5OSfbQ+as9+1aY+S5pqnJYJvXXNWFFJ6aspLkCyk4NMQ/pgtA== + dependencies: + "@babel/runtime" "^7.7.7" + ast-metadata-inferer "^0.1.1" + browserslist "^4.8.2" + caniuse-db "^1.0.30001017" + lodash.memoize "4.1.2" + mdn-browser-compat-data "^1.0.3" + semver "^6.3.0" + eslint-plugin-jasmine@^2.10.1: version "2.10.1" resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.10.1.tgz#5733b709e751f4bc40e31e1c16989bd2cdfbec97" @@ -4169,12 +4240,7 @@ espree@^4.0.0: acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -4380,7 +4446,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@3.0.2, extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -4447,6 +4513,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-glob@^2.0.2, fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" @@ -4674,9 +4745,9 @@ flatted@^2.0.0: integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== flow-parser@0.*: - version "0.116.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.116.0.tgz#7c08e4559a37863d95870e3ffb5523c5fc3525b1" - integrity sha512-sHiUjYI9U7H94diCN8BdzwYFJIkyCy2GN73UDFbKHTIuLdfROfZZwD6jAv2qWMl7lcPrBK9YAVeArLLbekxVeg== + version "0.116.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.116.1.tgz#2cded960269f702552feb5419c57c7c146ba171e" + integrity sha512-uMbaTjiMhBKa/il1esHyWyVVWfrWdG/eLmG62MQulZ59Yghpa30H1tmukFZLptsBafZ8ddiPyf7I+SiA+euZ6A== flush-write-stream@^1.0.0: version "1.1.1" @@ -4794,6 +4865,11 @@ fs-extra@^8.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -6135,12 +6211,18 @@ istextorbinary@^2.5.1: editions "^2.2.0" textextensions "^2.5.0" -iterate-value@^1.0.0: +iterate-iterator@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.1.tgz#d2003239b4a06c91a3f8092e379f6062b03c268c" - integrity sha512-xc6jTbwPOWEdD26y41BpJBqh/w3kuEcsQxTypXD+xYQA2+OZIfemmkm725cnRbm1cHj4SMLUO1+7oIA97e88gg== + resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" + integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw== + +iterate-value@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" + integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== dependencies: - es-get-iterator "^1.0.1" + es-get-iterator "^1.0.2" + iterate-iterator "^1.0.1" jasmine-ajax@^4.0.0: version "4.0.0" @@ -6517,11 +6599,6 @@ jest@24.9.0, jest@^24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" -jquery@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" - integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg== - js-cookie@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb" @@ -6830,9 +6907,9 @@ kind-of@^5.0.0: integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== klaw@^1.0.0: version "1.3.1" @@ -6983,7 +7060,7 @@ loader-runner@^2.3.0, loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3, loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.2.3: +loader-utils@1.2.3, loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -7055,6 +7132,11 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= +lodash.memoize@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.merge@^4.6.1: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -7274,6 +7356,13 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +mdn-browser-compat-data@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/mdn-browser-compat-data/-/mdn-browser-compat-data-1.0.5.tgz#016311130a2d1e21f283ecb77226fc3e9a23b187" + integrity sha512-9KDi2ERdtsFsSxz+w6+syybRHuCSR5WePONgM1ayE7GoC3odrCwhZNWgwDz+VK13oXq2qrjFpkSchHhwjE0UIg== + dependencies: + extend "3.0.2" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -7771,10 +7860,10 @@ node-notifier@^5.4.2: shellwords "^0.1.1" which "^1.3.0" -node-releases@^1.1.44: - version "1.1.45" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.45.tgz#4cf7e9175d71b1317f15ffd68ce63bce1d53e9f2" - integrity sha512-cXvGSfhITKI8qsV116u2FTzH5EWZJfgG7d4cpqwF8I8+1tWpD6AsvvGRKq2onR0DNj1jfqsjkXZsm14JMS7Cyg== +node-releases@^1.1.46: + version "1.1.47" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" + integrity sha512-k4xjVPx5FpwBUj0Gw7uvFOTF4Ep8Hok1I6qjwL3pLfwe7Y0REQSAqOwwv9TWBCUtMHxcXfY4PgRLRozcChvTcA== dependencies: semver "^6.3.0" @@ -7893,7 +7982,7 @@ object.assign@4.1.0, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.getownpropertydescriptors@^2.0.3: +object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== @@ -8463,15 +8552,6 @@ promise.allsettled@^1.0.1: function-bind "^1.1.1" iterate-value "^1.0.0" -promise.prototype.finally@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz#b8af89160c9c673cefe3b4c4435b53cfd0287067" - integrity sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.0" - function-bind "^1.1.1" - prompts@^2.0.1: version "2.3.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.0.tgz#a444e968fa4cc7e86689a74050685ac8006c4cc4" @@ -8555,11 +8635,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -q@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" - integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4= - q@^1.4.1, q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -8751,9 +8826,9 @@ read-pkg@^5.0.0: util-deprecate "~1.0.1" "readable-stream@2 || 3", readable-stream@^3.0.1, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + version "3.5.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" + integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -8832,7 +8907,7 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.3: +regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== @@ -9006,11 +9081,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reqwest@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/reqwest/-/reqwest-2.0.5.tgz#00fb15ac4918c419ca82b43f24c78882e66039a1" - integrity sha1-APsVrEkYxBnKgrQ/JMeIguZgOaE= - resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -9046,10 +9116,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2: - version "1.14.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" - integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.8.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5" + integrity sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw== dependencies: path-parse "^1.0.6" @@ -10323,9 +10393,9 @@ uglify-js@^2.8.29: uglify-to-browserify "~1.0.0" uglify-js@^3.1.4: - version "3.7.5" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.5.tgz#278c7c24927ac5a32d3336fc68fd4ae1177a486a" - integrity sha512-GFZ3EXRptKGvb/C1Sq6nO1iI7AGcjyqmIyOw0DrD0675e+NNbGO72xmMM2iEBdFbxaTLo70NbjM/Wy54uZIlsg== + version "3.7.6" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.6.tgz#0783daa867d4bc962a37cc92f67f6e3238c47485" + integrity sha512-yYqjArOYSxvqeeiYH2VGjZOqq6SVmhxzaPjJC1W2F9e+bqvFL9QXQ2osQuKUFjM2hGjKG2YclQnRKWQSt/nOTQ== dependencies: commander "~2.20.3" source-map "~0.6.1" @@ -10495,12 +10565,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" util@0.10.3: version "0.10.3" @@ -10527,9 +10599,9 @@ uuid@^2.0.1: integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== v8-compile-cache@2.0.3: version "2.0.3" @@ -10656,27 +10728,27 @@ wdio-chromedriver-service@^5.0.2: dependencies: fs-extra "^0.30.0" -webdriver@5.18.4: - version "5.18.4" - resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-5.18.4.tgz#5f88184754c70a76d436a3496ef41c6dc388dee0" - integrity sha512-9Wkzb/LldcGRniPHnmLicKGtdiQFMfezWwjv0mHlKx2B1kx/EhVDQ4gQjon03o891CEcazDKBcVh19eSJbZllQ== +webdriver@5.18.6: + version "5.18.6" + resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-5.18.6.tgz#95bede9ae84a7a0e9b90aba8e233f4de4a14f30a" + integrity sha512-n4rOV+OKxuxw0JjUySU5zDeR40As9wtwwaysGCdh2cD+CZsBUWITXJNrw8SjLQ59XupFeGksiEB+iJ0AfrLSuQ== dependencies: "@wdio/config" "5.18.4" "@wdio/logger" "5.16.10" "@wdio/protocols" "5.16.7" - "@wdio/utils" "5.16.15" + "@wdio/utils" "5.18.6" lodash.merge "^4.6.1" request "^2.83.0" -webdriverio@5.18.5: - version "5.18.5" - resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-5.18.5.tgz#100c1ae61eef35cffee3c17ae0ecfa2e9b001df4" - integrity sha512-1DR9l4eQb9SQNbiG/yywF7dpGoCWcUWpkO4dzUR7bSvhDo4hay/EK/5U5CYoxT13HfYV9QFKkpyOiXb2vMziqw== +webdriverio@5.18.6: + version "5.18.6" + resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-5.18.6.tgz#a86000fc65bf4c33e7f089dd86e97932ad4d5e3a" + integrity sha512-eJ7bIpjAqxHkeRvsf+fysqv98e4mNQ/9Zn+CBRDbS/+/EcmOKHghLdFSFEh019JoglMY7xYj3+mBDCsC7+aGLA== dependencies: "@wdio/config" "5.18.4" "@wdio/logger" "5.16.10" - "@wdio/repl" "5.16.15" - "@wdio/utils" "5.16.15" + "@wdio/repl" "5.18.6" + "@wdio/utils" "5.18.6" archiver "^3.0.0" css-value "^0.0.1" grapheme-splitter "^1.0.2" @@ -10687,7 +10759,7 @@ webdriverio@5.18.5: resq "^1.6.0" rgb2hex "^0.1.0" serialize-error "^5.0.0" - webdriver "5.18.4" + webdriver "5.18.6" webidl-conversions@^4.0.2: version "4.0.2"