diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc35fae..ea93bd31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# 7.0.0 + +### Breaking Changes + +- [#275](https://github.com/okta/okta-react/pull/275) + - `SecureRoute` should be imported from `@okta/okta-react/react-router-5` + +### Features + +- [#275](https://github.com/okta/okta-react/pull/275) + - Adds new component `` for integration with `react-router 6.x`. It should be imported from `@okta/okta-react/react-router-6` + +# 6.9.0 + +### Bug Fixes + +-[#284](https://github.com/okta/okta-react/pull/284) fix: passes the return value of `restoreOriginalUri()`, so promises will be awaited + # 6.8.0 ### Bug Fixes diff --git a/README.md b/README.md index 83cf3ea4..217fcecf 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,7 @@ npm install --save react-router-dom # see note below npm install --save @okta/okta-auth-js # requires at least version 5.3.1 ``` -> ⚠️ NOTE ⚠️
The [SecureRoute](#secureroute) component packaged in this SDK only works with `react-router-dom` `5.x`. -If you're using `react-router-dom` `6.x`, you'll have to write your own `SecureRoute` component.

See these [samples](https://github.com/okta/okta-react/tree/master/samples/routing) to get started +See these [samples](https://github.com/okta/okta-react/tree/master/samples/routing) to get started ## Usage @@ -112,9 +111,10 @@ If you're using `react-router-dom` `6.x`, you'll have to write your own `SecureR `okta-react` provides a number of pre-built components to connect a `react-router`-based SPA to Okta OIDC information. You can use these components directly, or use them as a basis for building your own components. - [SecureRoute](#secureroute) - A normal `Route` except authentication is needed to render the component. +- [SecureOutlet](#secureoutlet) - A route component to be used for rendering routes with authentication requirement. + +> ⚠️ NOTE ⚠️
The [SecureRoute](#secureroute) component packaged in this SDK only works with `react-router-dom` `5.x`.
The [SecureOutlet](#secureoutlet) component works with `react-router-dom` `6.x`. -> ⚠️ NOTE ⚠️
The [SecureRoute](#secureroute) component packaged in this SDK only works with `react-router-dom` `5.x`. -If you're using `react-router-dom` `6.x`, you'll have to write your own `SecureRoute` component.

See these [samples](https://github.com/okta/okta-react/tree/master/samples/routing) to get started ### General components @@ -147,14 +147,15 @@ This example defines 3 routes: > A common mistake is to try and apply an authentication requirement to all pages, THEN add an exception for the login page. This often fails because of how routes are evaluated in most routing packages. To avoid this problem, declare specific routes or branches of routes that require authentication without exceptions. -#### Creating React Router Routes with class-based components +#### Creating React Router 5 Routes with class-based components ```jsx // src/App.js import React, { Component } from 'react'; import { BrowserRouter as Router, Route, withRouter } from 'react-router-dom'; -import { SecureRoute, Security, LoginCallback } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; import Home from './Home'; import Protected from './Protected'; @@ -191,13 +192,14 @@ export default class extends Component { } ``` -#### Creating React Router Routes with function-based components +#### Creating React Router 6 Routes with function-based components ```jsx import React from 'react'; -import { SecureRoute, Security, LoginCallback } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureOutlet } from '@okta/okta-react/react-route-6'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; -import { BrowserRouter as Router, Route, useHistory } from 'react-router-dom'; +import { BrowserRouter as Router, Route, useNavigate } from 'react-router-dom'; import Home from './Home'; import Protected from './Protected'; @@ -208,16 +210,20 @@ const oktaAuth = new OktaAuth({ }); const App = () => { - const history = useHistory(); - const restoreOriginalUri = async (_oktaAuth, originalUri) => { - history.replace(toRelativeUrl(originalUri || '/', window.location.origin)); - }; + const navigate = useNavigate(); + const restoreOriginalUri = React.useCallback(async (_oktaAuth, originalUri) => { + navigate(toRelativeUrl(originalUri || '/', window.location.origin), { replace: true }); + }, [navigate]); return ( - - - + + } /> + }> + } /> + + } /> + ); }; @@ -231,6 +237,77 @@ const AppWithRouterAccess = () => ( export default AppWithRouterAccess; ``` +#### Creating React Router 6.4+ Routes supporting Data API with function-based components + +```jsx +// src/App.js + +import React from 'react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureOutlet } from '@okta/okta-react/react-route-6'; +import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; +import { RouterProvider, createBrowserRouter, Outlet, useNavigate } from 'react-router-dom'; +import Home from './Home'; +import Protected from './Protected'; + +const oktaAuth = new OktaAuth({ + issuer: 'https://{yourOktaDomain}/oauth2/default', + clientId: '{clientId}', + redirectUri: window.location.origin + '/login/callback' +}); + +const Layout = () => { + const navigate = useNavigate(); + const restoreOriginalUri = React.useCallback((_oktaAuth, originalUri) => { + navigate(toRelativeUrl(originalUri || '/', window.location.origin), { replace: true }); + }, [navigate]); + + return ( + + + + ); +}; + +const routes = [ + { + path: '/', + element: , + children: [ + { + path: '', + element: , + }, + { + path: 'login/callback', + element: , + }, + { + path: 'protected', + element: , + children: [ + { + path: '', + element: , + } + ], + }, + ], + } +]; + +const router = createBrowserRouter(routes); + +const AppWithRouterAccess = () => ( + +); + +export default AppWithRouterAccess; +``` + #### Show Login and Logout Buttons (class-based) ```jsx @@ -395,16 +472,16 @@ export default MessageList = () => { #### restoreOriginalUri -*(required)* Callback function. Called to restore original URI during [oktaAuth.handleLoginRedirect()](https://github.com/okta/okta-auth-js#handleloginredirecttokens) is called. Will override [restoreOriginalUri option of oktaAuth](https://github.com/okta/okta-auth-js#restoreoriginaluri) +*(required)* Callback function. Called to restore original URI during [oktaAuth.handleLoginRedirect()](https://github.com/okta/okta-auth-js#handleloginredirecttokens) is called. Will override [restoreOriginalUri option of oktaAuth](https://github.com/okta/okta-auth-js#restoreoriginaluri). Please use the appropriate navigate functions for your router: [useNavigate](https://reactrouter.com/en/main/hooks/use-navigate) for `react-router 6.x`, [useHistory](https://v5.reactrouter.com/web/api/Hooks/usehistory) for `reat-router 5.x`. #### onAuthRequired -*(optional)* Callback function. Called when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive [oktaAuth][Okta Auth SDK] instance as the first function parameter. This is triggered when a [SecureRoute](#secureroute) is accessed without authentication. A common use case for this callback is to redirect users to a custom login route when authentication is required for a [SecureRoute](#secureroute). +*(optional)* Callback function. Called when authentication is required. If this is not supplied, `okta-react` redirects to Okta. This callback will receive [oktaAuth][Okta Auth SDK] instance as the first function parameter. This is triggered when a [SecureRoute](#secureroute) or [SecureOutlet](#secureoutlet) is accessed without authentication. A common use case for this callback is to redirect users to a custom login route when authentication is required. #### Example ```jsx -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; const oktaAuth = new OktaAuth({ @@ -414,17 +491,17 @@ const oktaAuth = new OktaAuth({ }); export default App = () => { - const history = useHistory(); + const navigate = useNavigate(); - const customAuthHandler = (oktaAuth) => { + const customAuthHandler = React.useCallback((oktaAuth) => { // Redirect to the /login page that has a CustomLoginComponent - // This example is specific to React-Router + // This example is specific to React-Router 6 history.push('/login'); - }; + }, [navigate]); - const restoreOriginalUri = async (_oktaAuth, originalUri) => { - history.replace(toRelativeUrl(originalUri || '/', window.location.origin)); - }; + const restoreOriginalUri = React.useCallback(async (_oktaAuth, originalUri) => { + navigate(toRelativeUrl(originalUri || '/', window.location.origin), { replace: true }); + }, [navigate]); return ( { onAuthRequired={customAuthHandler} restoreOriginalUri={restoreOriginalUri} > - - {/* some routes here */} + + } /> + {/* some routes here */} + ); }; @@ -459,8 +538,10 @@ class App extends Component { render() { return ( - - + + } /> + } /> + ); } @@ -472,6 +553,8 @@ class App extends Component { `SecureRoute` ensures that a route is only rendered if the user is authenticated. If the user is not authenticated, it calls [onAuthRequired](#onauthrequired) if it exists, otherwise, it redirects to Okta. +> ⚠️ NOTE ⚠️
The [SecureRoute](#secureroute) component packaged in this SDK only works with `react-router-dom` `5.x`. + #### onAuthRequired `SecureRoute` accepts `onAuthRequired` as an optional prop, it overrides [onAuthRequired](#onauthrequired) from the [Security](#security) component if exists. @@ -482,7 +565,7 @@ class App extends Component { #### `react-router` related props -`SecureRoute` integrates with `react-router`. Other routers will need their own methods to ensure authentication using the hooks/HOC props provided by this SDK. +`SecureRoute` integrates with `react-router` `5.x`. Other routers will need their own methods to ensure authentication using the hooks/HOC props provided by this SDK. As with `Route` from `react-router-dom`, `` can take one of: @@ -490,6 +573,32 @@ As with `Route` from `react-router-dom`, `` can take one of: - a `render` prop that is passed a function that returns a component. This function will be passed any additional props that react-router injects (such as `history` or `match`) - children components +### `SecureOutlet` + +`SecureOutlet` is a component for a route that renders its child route elements only if the user is authenticated. If the user is not authenticated, it calls [onAuthRequired](#onauthrequired) if it exists, otherwise, it redirects to Okta. + +> ⚠️ NOTE ⚠️
The [SecureOutlet](#secureoutlet) component works with `react-router-dom` `6.x`. + +Example: +```jsx +}> + } /> + } /> + +``` + +#### onAuthRequired + +`SecureOutlet` accepts `onAuthRequired` as an optional prop, it overrides [onAuthRequired](#onauthrequired) from the [Security](#security) component if exists. + +#### errorComponent + +`SecureOutlet` runs internal `handleLogin` process which may throw Error when `authState.isAuthenticated` is false. By default, the Error will be rendered with `OktaError` component. If you wish to customise the display of such error messages, you can pass your own component as an `errorComponent` prop to ``. The error value will be passed to the `errorComponent` as the `error` prop. + +#### loadingElement + +By default, `SecureOutlet` will display nothing during redirect to a login page or running `onAuthRequired`. If you wish to customize this, you can pass your React element (not component) as `loadingElement` prop to ``. Example: `

Loading...

` + ### `LoginCallback` `LoginCallback` handles the callback after the redirect to and back from the Okta-hosted login page. By default, it parses the tokens from the uri, stores them, then redirects to `/`. If a `SecureRoute` caused the redirect, then the callback redirects to the secured route. For more advanced cases, this component can be copied to your own source tree and modified as needed. @@ -511,23 +620,26 @@ An `interaction_required` error is an indication that you should resume the auth If using the [Okta SignIn Widget][], redirecting to your login route will allow the widget to automatically resume your authentication transaction. ```jsx -// Example assumes you are using react-router with a customer-hosted Okta SignIn Widget on your /login route +// Example assumes you are using react-router 6 with a customer-hosted Okta SignIn Widget on your /login route // This code is wherever you have your component, which must be inside your for react-router - const onAuthResume = async () => { - history.push('/login'); - }; +const navigate = useNavigate(); +const onAuthResume = React.useCallback(async () => { + navigate('/login'); +}, [navigate]); return ( - - - } /> - - - + + } /> + }> + } /> + + } /> + } /> + ); ``` @@ -560,6 +672,20 @@ export default MyComponent = () => { ## Migrating between versions +### Migrating from 6.x to 7.x + +If you are using `react-router` `5.x` with [SecureRoute](#secureroute), you need to change an import from +```jsx +import { SecureRoute } from '@okta/okta-react'; +``` +to +```jsx +import { SecureRoute } from '@okta/okta-react/react-router-5'; +``` + +If you are using `react-router` `6.x`, please use [SecureOutlet](#secureoutlet). + + ### Migrating from 5.x to 6.x `@okta/okta-react` 6.x requires `@okta/okta-auth-js` 5.x (see [notes for migration](https://github.com/okta/okta-auth-js/#from-4x-to-5x)). Some changes affects `@okta/okta-react`: diff --git a/build.js b/build.js index 5a19633e..c4bdbe81 100644 --- a/build.js +++ b/build.js @@ -35,6 +35,7 @@ delete packageJSON.scripts; // remove all scripts delete packageJSON.jest; // remove jest section delete packageJSON['jest-junit']; // remove jest-junit section delete packageJSON.workspaces; // remove yarn workspace section +delete packageJSON.devDependencies; // Remove "build/" from the entrypoint paths. ['main', 'module', 'types'].forEach(function(key) { @@ -43,6 +44,14 @@ delete packageJSON.workspaces; // remove yarn workspace section } }); +['.', './react-router-5', './react-router-6'].forEach(function(name) { + ['types', 'import', 'require', 'default'].forEach(function(key) { + if (packageJSON['exports'][name][key]) { + packageJSON['exports'][name][key] = packageJSON['exports'][name][key].replace(`${NPM_DIR}/`, ''); + } + }); +}); + fs.writeFileSync(`./${NPM_DIR}/package.json`, JSON.stringify(packageJSON, null, 4)); shell.echo(chalk.green(`End building`)); diff --git a/jest.config.js b/jest.config.js index 071bd962..2b57a8f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,10 @@ module.exports = { // avoid react conflict in yarn workspace '^react$': '/node_modules/react', '^react-dom$': '/node_modules/react-dom', - '^react-router-dom$': '/node_modules/react-router-dom' + '^react-router-dom$': '/node_modules/react-router-dom', + '^@okta/okta-react$': '/src', + '^@okta/okta-react/react-router-5$': '/src/react-router-5.ts', + '^@okta/okta-react/react-router-6$': '/src/react-router-6.ts', }, roots: [ './test/jest' diff --git a/package.json b/package.json index 7760f3fe..6009878f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@okta/okta-react", - "version": "6.8.0", + "version": "6.10.0", "description": "React support for Okta", "private": true, "scripts": { @@ -18,6 +18,25 @@ "dev": "yarn bundle --watch", "generate": "yarn --cwd generator install && yarn --cwd generator generate" }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/bundles/types/index.d.ts", + "import": "./dist/bundles/okta-react.esm.js", + "require": "./dist/bundles/okta-react.cjs.js", + "default": "./dist/bundles/okta-react.umd.js" + }, + "./react-router-5": { + "types": "./dist/bundles/types/react-router-5.d.ts", + "import": "./dist/bundles/okta-react-router-5.esm.js", + "require": "./dist/bundles/okta-react-router-5.cjs.js" + }, + "./react-router-6": { + "types": "./dist/bundles/types/react-router-6.d.ts", + "import": "./dist/bundles/okta-react-router-6.esm.js", + "require": "./dist/bundles/okta-react-router-6.cjs.js" + } + }, "repository": { "type": "git", "url": "git+https://github.com/okta/okta-react.git" @@ -43,7 +62,8 @@ "//": "set-value@2.0.1 has a vuln, see OKTA-473553", "**/set-value": "^4.1.0", "ejs": "^3.1.7", - "axios": "^0.27.2" + "axios": "^0.27.2", + "**/glob": "^9.3.5" }, "dependencies": { "@babel/runtime": "^7.11.2", @@ -55,13 +75,18 @@ "react-dom": ">=16.8.0", "react-router-dom": ">=5.1.0" }, + "peerDependenciesMeta": { + "react-router-dom": { + "optional": true + } + }, "devDependencies": { "@babel/cli": "^7.19.3", "@babel/core": "^7.19.3", "@babel/plugin-transform-runtime": "^7.19.1", "@babel/preset-env": "^7.19.3", "@babel/preset-react": "^7.18.6", - "@okta/okta-auth-js": "^7.0.0", + "@okta/okta-auth-js": "^7.7.0", "@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-replace": "^2.3.4", "@testing-library/jest-dom": "^5.16.2", @@ -99,6 +124,7 @@ "react": "^16.9.0", "react-dom": "^16.9.0", "react-router-dom": "5.2.0", + "react-router-dom6": "npm:react-router-dom@^6.22.0", "rimraf": "^2.6.2", "rollup": "^2.33.1", "rollup-plugin-cleanup": "^3.2.1", @@ -135,4 +161,4 @@ "**/@types/react-router-dom" ] } -} +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index cf450dc0..dac5e7b2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,6 +10,7 @@ const makeExternalPredicate = () => { const externalArr = [ ...Object.keys(pkg.peerDependencies || {}), ...Object.keys(pkg.dependencies || {}), + '@okta/okta-react', ]; if (externalArr.length === 0) { @@ -107,5 +108,69 @@ export default [ sourcemap: true } ] + }, + { + input: 'src/react-router-5.ts', + external, + plugins: [ + ...commonPlugins, + babel({ + babelHelpers: 'runtime', + presets: [ + '@babel/preset-env', + '@babel/preset-react' + ], + plugins: [ + '@babel/plugin-transform-runtime' + ], + extensions + }), + ], + output: [ + { + format: 'cjs', + file: 'dist/bundles/okta-react-router-5.cjs.js', + exports: 'named', + sourcemap: true + }, + { + format: 'esm', + file: 'dist/bundles/okta-react-router-5.esm.js', + exports: 'named', + sourcemap: true + } + ] + }, + { + input: 'src/react-router-6.ts', + external, + plugins: [ + ...commonPlugins, + babel({ + babelHelpers: 'runtime', + presets: [ + '@babel/preset-env', + '@babel/preset-react' + ], + plugins: [ + '@babel/plugin-transform-runtime' + ], + extensions + }), + ], + output: [ + { + format: 'cjs', + file: 'dist/bundles/okta-react-router-6.cjs.js', + exports: 'named', + sourcemap: true + }, + { + format: 'esm', + file: 'dist/bundles/okta-react-router-6.esm.js', + exports: 'named', + sourcemap: true + } + ] } ]; diff --git a/samples/custom-login/src/App.jsx b/samples/custom-login/src/App.jsx index 8812543a..f22ffe2c 100644 --- a/samples/custom-login/src/App.jsx +++ b/samples/custom-login/src/App.jsx @@ -13,7 +13,8 @@ import React from 'react'; import { Route, useHistory, Switch } from 'react-router-dom'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; -import { Security, SecureRoute, LoginCallback } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; import { Container } from 'semantic-ui-react'; import config from './config'; import Home from './Home'; diff --git a/samples/doc-direct-auth/src/App.jsx b/samples/doc-direct-auth/src/App.jsx index 4e8116d8..990b58ea 100644 --- a/samples/doc-direct-auth/src/App.jsx +++ b/samples/doc-direct-auth/src/App.jsx @@ -12,7 +12,8 @@ import React from 'react'; import { Route, useHistory } from 'react-router-dom'; -import { Security, SecureRoute, LoginCallback } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; import Home from './Home'; import Login from './Login'; diff --git a/samples/doc-embedded-widget/src/App.jsx b/samples/doc-embedded-widget/src/App.jsx index 4e8116d8..990b58ea 100644 --- a/samples/doc-embedded-widget/src/App.jsx +++ b/samples/doc-embedded-widget/src/App.jsx @@ -12,7 +12,8 @@ import React from 'react'; import { Route, useHistory } from 'react-router-dom'; -import { Security, SecureRoute, LoginCallback } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; import Home from './Home'; import Login from './Login'; diff --git a/samples/okta-hosted-login/src/App.jsx b/samples/okta-hosted-login/src/App.jsx index 32ab1b3d..fbadc2ce 100644 --- a/samples/okta-hosted-login/src/App.jsx +++ b/samples/okta-hosted-login/src/App.jsx @@ -13,7 +13,8 @@ import React from 'react'; import { Route, useHistory, Switch } from 'react-router-dom'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; -import { Security, SecureRoute, LoginCallback } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; import { Container } from 'semantic-ui-react'; import config from './config'; import Home from './Home'; diff --git a/samples/routing/react-router-dom-v6-hash/README.md b/samples/routing/react-router-dom-v6-hash/README.md index 51307382..422ed915 100644 --- a/samples/routing/react-router-dom-v6-hash/README.md +++ b/samples/routing/react-router-dom-v6-hash/README.md @@ -2,7 +2,6 @@ Sample app to demonstrate how to use `react-router-dom` v6 with `@okta/okta-react`. -**NOTE:** This sample is not runnable before fixing issue [#187](https://github.com/okta/okta-react/issues/187). See [this comment](https://github.com/okta/okta-react/issues/187#issuecomment-1043059092) for potential workaround. ## Install ```bash diff --git a/samples/routing/react-router-dom-v6/README.md b/samples/routing/react-router-dom-v6/README.md index 5e9310b4..04a810d7 100644 --- a/samples/routing/react-router-dom-v6/README.md +++ b/samples/routing/react-router-dom-v6/README.md @@ -2,7 +2,6 @@ Sample app to demonstrate how to use `react-router-dom` v6 with `@okta/okta-react`. -**NOTE:** This sample is not runnable before fixing issue [#187](https://github.com/okta/okta-react/issues/187). See [this comment](https://github.com/okta/okta-react/issues/187#issuecomment-1043059092) for potential workaround. ## Install ```bash diff --git a/src/LoginCallback.tsx b/src/LoginCallback.tsx index d0860eee..22efa2de 100644 --- a/src/LoginCallback.tsx +++ b/src/LoginCallback.tsx @@ -14,7 +14,7 @@ import * as React from 'react'; import { useOktaAuth, OnAuthResumeFunction } from './OktaContext'; import OktaError from './OktaError'; -interface LoginCallbackProps { +export interface LoginCallbackProps { errorComponent?: React.ComponentType<{ error: Error }>; onAuthResume?: OnAuthResumeFunction; loadingElement?: React.ReactElement; diff --git a/src/OktaContext.ts b/src/OktaContext.ts index bdab0a93..0d7e20fe 100644 --- a/src/OktaContext.ts +++ b/src/OktaContext.ts @@ -25,6 +25,7 @@ export interface IOktaContext { const OktaContext = React.createContext(null); -export const useOktaAuth = (): IOktaContext => React.useContext(OktaContext) as IOktaContext; +export const useOktaAuth = (context?: typeof OktaContext): IOktaContext => + React.useContext(context ?? OktaContext) as IOktaContext; export default OktaContext; diff --git a/src/OktaError.tsx b/src/OktaError.tsx index 1054424f..6b6380a4 100644 --- a/src/OktaError.tsx +++ b/src/OktaError.tsx @@ -12,7 +12,9 @@ import * as React from 'react'; -const OktaError: React.FC<{ error: Error }> = ({ error }) => { +export type ErrorComponent = React.FC<{ error: Error }>; + +const OktaError: ErrorComponent = ({ error }) => { if(error.name && error.message) { return

{error.name}: {error.message}

; } diff --git a/src/SecureOutlet.tsx b/src/SecureOutlet.tsx new file mode 100644 index 00000000..f36a4993 --- /dev/null +++ b/src/SecureOutlet.tsx @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2017-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. + */ + +import * as React from 'react'; +import { useOktaAuth, OnAuthRequiredFunction } from './OktaContext'; +import * as ReactRouterDom from 'react-router-dom'; +import { toRelativeUrl, AuthSdkError } from '@okta/okta-auth-js'; +// Important! Don't import OktaContext from './OktaContext' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line import/no-extraneous-dependencies +import { OktaContext } from '@okta/okta-react'; +import OktaError from './OktaError'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let Outlet: any; +if ('Outlet' in ReactRouterDom) { + // trick static analyzer to avoid "'Outlet' is not exported" error + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Outlet = (ReactRouterDom as any)['Outlet' in ReactRouterDom ? 'Outlet' : '']; +} else { + // throw when useMatch is triggered + Outlet = () => { + throw new AuthSdkError('Unsupported: SecureOutlet only works with react-router-dom v6 or any router library with compatible APIs. See examples under the "samples" folder for how to implement your own custom SecureRoute Component.'); + }; +} + +export interface SecureOutletProps { + onAuthRequired?: OnAuthRequiredFunction; + errorComponent?: React.ComponentType<{ error: Error }>; + loadingElement?: React.ReactElement; +} + +const SecureOutlet: React.FC> = ({ + onAuthRequired, + errorComponent, + loadingElement = null, + ...props +}) => { + // Need to use OktaContext imported from `@okta/okta-react` + // Because SecureOutlet needs to be imported from `@okta/okta-react/react-router-6` + const { oktaAuth, authState, _onAuthRequired } = useOktaAuth(OktaContext); + const pendingLogin = React.useRef(false); + const [handleLoginError, setHandleLoginError] = React.useState(null); + const ErrorReporter = errorComponent || OktaError; + + React.useEffect(() => { + const handleLogin = async () => { + if (pendingLogin.current) { + return; + } + + pendingLogin.current = true; + + const originalUri = toRelativeUrl(window.location.href, window.location.origin); + oktaAuth.setOriginalUri(originalUri); + const onAuthRequiredFn = onAuthRequired || _onAuthRequired; + if (onAuthRequiredFn) { + await onAuthRequiredFn(oktaAuth); + } else { + await oktaAuth.signInWithRedirect(); + } + }; + + if (!authState) { + return; + } + + if (authState.isAuthenticated) { + pendingLogin.current = false; + return; + } + + // Start login if app has decided it is not logged in and there is no pending signin + if(!authState.isAuthenticated) { + handleLogin().catch(err => { + setHandleLoginError(err as Error); + }); + } + + }, [ + authState, + oktaAuth, + onAuthRequired, + _onAuthRequired + ]); + + if (handleLoginError) { + return ; + } + + if (authState?.isAuthenticated) { + return ( + + ); + } + + return loadingElement; +}; + +export default SecureOutlet; diff --git a/src/SecureRoute.tsx b/src/SecureRoute.tsx index 0d114fb4..e97a1075 100644 --- a/src/SecureRoute.tsx +++ b/src/SecureRoute.tsx @@ -14,6 +14,11 @@ import * as React from 'react'; import { useOktaAuth, OnAuthRequiredFunction } from './OktaContext'; import * as ReactRouterDom from 'react-router-dom'; import { toRelativeUrl, AuthSdkError } from '@okta/okta-auth-js'; +// Important! Don't import OktaContext from './OktaContext' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line import/no-extraneous-dependencies +import { OktaContext } from '@okta/okta-react'; import OktaError from './OktaError'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -36,8 +41,10 @@ const SecureRoute: React.FC<{ onAuthRequired, errorComponent, ...routeProps -}) => { - const { oktaAuth, authState, _onAuthRequired } = useOktaAuth(); +}) => { + // Need to use OktaContext imported from `@okta/okta-react` + // Because SecureRoute needs to be imported from `@okta/okta-react/react-router-5` + const { oktaAuth, authState, _onAuthRequired } = useOktaAuth(OktaContext); const match = useMatch(routeProps); const pendingLogin = React.useRef(false); const [handleLoginError, setHandleLoginError] = React.useState(null); diff --git a/src/Security.tsx b/src/Security.tsx index b9a1bd3d..fedbaca9 100644 --- a/src/Security.tsx +++ b/src/Security.tsx @@ -24,12 +24,16 @@ declare const PACKAGE_NAME: string; declare const PACKAGE_VERSION: string; declare const SKIP_VERSION_CHECK: string; -const Security: React.FC<{ +export interface SecurityProps { oktaAuth: OktaAuth, restoreOriginalUri: RestoreOriginalUriFunction, onAuthRequired?: OnAuthRequiredFunction, children?: React.ReactNode -} & React.HTMLAttributes> = ({ +} + +let restoreOriginalUriOverridden = false; + +const Security: React.FC> = ({ oktaAuth, restoreOriginalUri, onAuthRequired, @@ -47,19 +51,23 @@ const Security: React.FC<{ return; } + // Fixes issue #227: Prevents multiple effect calls in React18 StrictMode + // Top-level variable solution follows: https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application + if (restoreOriginalUriOverridden) { + return; + } + // Add default restoreOriginalUri callback // props.restoreOriginalUri is required, therefore if options.restoreOriginalUri exists, there are 2 callbacks - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore if (oktaAuth.options.restoreOriginalUri) { console.warn('Two custom restoreOriginalUri callbacks are detected. The one from the OktaAuth configuration will be overridden by the provided restoreOriginalUri prop from the Security component.'); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore oktaAuth.options.restoreOriginalUri = (async (oktaAuth: unknown, originalUri: string) => { - restoreOriginalUri(oktaAuth as OktaAuth, originalUri); + return restoreOriginalUri(oktaAuth as OktaAuth, originalUri); }) as ((oktaAuth: OktaAuth, originalUri?: string) => Promise); - + restoreOriginalUriOverridden = true; }, []); // empty array, only check on component mount React.useEffect(() => { diff --git a/src/index.ts b/src/index.ts index b73df6a2..91c730a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ import Security from './Security'; import withOktaAuth from './withOktaAuth'; import OktaContext, { useOktaAuth } from './OktaContext'; import LoginCallback from './LoginCallback'; -import SecureRoute from './SecureRoute'; export { Security, @@ -22,5 +21,4 @@ export { useOktaAuth, OktaContext, LoginCallback, - SecureRoute, }; diff --git a/src/react-router-5.ts b/src/react-router-5.ts new file mode 100644 index 00000000..c0526d4f --- /dev/null +++ b/src/react-router-5.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2017-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. + */ + +import SecureRoute from './SecureRoute'; + +export { SecureRoute }; diff --git a/src/react-router-6.ts b/src/react-router-6.ts new file mode 100644 index 00000000..8e2ed45f --- /dev/null +++ b/src/react-router-6.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2017-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. + */ + +import SecureOutlet from './SecureOutlet'; + +export { SecureOutlet }; diff --git a/test/apps/test-harness-app/src/App.tsx b/test/apps/test-harness-app/src/App.tsx index 98d34f24..bddd7ac5 100644 --- a/test/apps/test-harness-app/src/App.tsx +++ b/test/apps/test-harness-app/src/App.tsx @@ -13,7 +13,8 @@ import * as React from 'react'; import { Route, Switch, useHistory } from 'react-router-dom'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; -import { Security, LoginCallback, SecureRoute } from '@okta/okta-react'; +import { Security, LoginCallback } from '@okta/okta-react'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; import Home from './Home'; import Protected from './Protected'; import CustomLogin from './CustomLogin'; diff --git a/test/apps/v6-app/src/App.tsx b/test/apps/v6-app/src/App.tsx index 87b10965..3598109b 100644 --- a/test/apps/v6-app/src/App.tsx +++ b/test/apps/v6-app/src/App.tsx @@ -14,7 +14,7 @@ import * as React from 'react'; import { Route, Routes, useNavigate } from 'react-router-dom'; import { OktaAuth, toRelativeUrl } from '@okta/okta-auth-js'; import { Security, LoginCallback } from '@okta/okta-react'; -import { SecureRoute } from './SecureRoute'; +import { SecureOutlet } from '@okta/okta-react/react-router-6'; import Home from './Home'; import Protected from './Protected'; import CustomLogin from './CustomLogin'; @@ -51,7 +51,7 @@ const App: React.FC<{ } /> } /> } /> - }> + }> } /> } /> diff --git a/test/apps/v6-app/src/SecureRoute.tsx b/test/apps/v6-app/src/SecureRoute.tsx deleted file mode 100644 index e50be655..00000000 --- a/test/apps/v6-app/src/SecureRoute.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/*! - * Copyright (c) 2017-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. - */ - -import React, { useEffect } from 'react'; -import { useOktaAuth } from '@okta/okta-react'; -import { toRelativeUrl } from '@okta/okta-auth-js'; -import { Outlet } from 'react-router-dom'; - - -export const SecureRoute: React.FC = () => { - const { oktaAuth, authState } = useOktaAuth(); - - useEffect(() => { - if (!authState) { - return; - } - - if (!authState?.isAuthenticated) { - const originalUri = toRelativeUrl(window.location.href, window.location.origin); - oktaAuth.setOriginalUri(originalUri); - oktaAuth.signInWithRedirect(); - } - }, [oktaAuth, !!authState, authState?.isAuthenticated]); - - if (!authState || !authState?.isAuthenticated) { - return (

Loading...

); - } - - return (); -} diff --git a/test/jest/loginCallback.test.tsx b/test/jest/loginCallback.test.tsx index 9dfb553d..5ed9edbd 100644 --- a/test/jest/loginCallback.test.tsx +++ b/test/jest/loginCallback.test.tsx @@ -12,18 +12,21 @@ import React from 'react' import { mount } from 'enzyme'; +import { AuthState, OktaAuth } from '@okta/okta-auth-js'; +import { SecurityProps } from '../../src/Security'; +import { LoginCallbackProps } from '../../src/LoginCallback'; /* Forces Jest to use same version of React to enable fresh module state via isolateModulesAsync() call in beforeEach(). Otherwise, React raises "Invalid hook call" error because of multiple copies of React, see: https://github.com/jestjs/jest/issues/11471#issuecomment-851266333 */ jest.mock('react', () => jest.requireActual('react')); describe('', () => { - let oktaAuth: any; - let authState: any; - let mockProps: any; - let Security: any; - let LoginCallback: any; - const restoreOriginalUri = async (_: any, url: string) => { + let oktaAuth: OktaAuth; + let authState: AuthState | null; + let mockProps: SecurityProps; + let Security: React.FC; + let LoginCallback: React.FC; + const restoreOriginalUri = async (_: OktaAuth, url: string) => { location.href = url; }; beforeEach(async () => { @@ -43,7 +46,7 @@ describe('', () => { idx: { isInteractionRequired: jest.fn().mockImplementation( () => false ), } - }; + } as any as OktaAuth; mockProps = { oktaAuth, restoreOriginalUri @@ -117,7 +120,7 @@ describe('', () => { it('can be passed a custom component to render', () => { authState = { isAuthenticated: true, - error: { has: 'errorData' } + error: { has: 'errorData' } as any }; const MyErrorComponent = (props: any) => { diff --git a/test/jest/oktaError.test.tsx b/test/jest/oktaError.test.tsx index 4eb9a39e..a97b29d6 100644 --- a/test/jest/oktaError.test.tsx +++ b/test/jest/oktaError.test.tsx @@ -37,7 +37,9 @@ describe('', () => { const errorCode = '400'; const errorLink = 'http://errorlink.com'; const errorId = 'fake error id'; - const errorCauses = ['fake error cause']; + const errorCauses = [{ + errorSummary: 'fake error cause' + }]; const error = new AuthApiError({ errorSummary, errorCode, errorLink, errorId, errorCauses }); const wrapper = mount( diff --git a/test/jest/reactRouterV6.test.tsx b/test/jest/reactRouterV6.test.tsx deleted file mode 100644 index bd5ffd0d..00000000 --- a/test/jest/reactRouterV6.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/*! - * Copyright (c) 2017-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. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as React from 'react'; -import { act } from 'react-dom/test-utils'; -import { render } from 'react-dom'; -import SecureRoute from '../../src/SecureRoute'; -import OktaContext from '../../src/OktaContext'; -import { AuthSdkError } from '@okta/okta-auth-js'; - -jest.mock('react-router-dom', () => ({ - __esModule: true, - useMatch: jest.fn() -})); - -class ErrorBoundary extends React.Component { - constructor(props: any) { - super(props); - this.state = { - error: null - } as { - error: AuthSdkError | null - }; - } - - componentDidCatch(error: AuthSdkError) { - this.setState({ error: error }); - } - - render() { - if (this.state.error) { - // You can render any custom fallback UI - return

{ this.state.error.toString() }

; - } - - return this.props.children; - } -} - -describe('react-router-dom v6', () => { - let oktaAuth: any; - let authState: any; - - beforeEach(() => { - // prevents logging error to console - // eslint-disable-next-line @typescript-eslint/no-empty-function - console.error = (()=>{}); // noop - - authState = null; - oktaAuth = { - options: {}, - authStateManager: { - getAuthState: jest.fn().mockImplementation(() => authState), - subscribe: jest.fn(), - unsubscribe: jest.fn(), - updateAuthState: jest.fn(), - }, - isLoginRedirect: jest.fn().mockImplementation(() => false), - handleLoginRedirect: jest.fn(), - signInWithRedirect: jest.fn(), - setOriginalUri: jest.fn(), - start: jest.fn(), - }; - }); - - it('throws unsupported error', async () => { - const container = document.createElement('div'); - await act(async () => { - render( - - - - - , - container - ); - }); - expect(container.innerHTML).toBe('

AuthSdkError: Unsupported: SecureRoute only works with react-router-dom v5 or any router library with compatible APIs. See examples under the "samples" folder for how to implement your own custom SecureRoute Component.

'); - }) -}); \ No newline at end of file diff --git a/test/jest/secureOutlet.test.tsx b/test/jest/secureOutlet.test.tsx new file mode 100644 index 00000000..608bc607 --- /dev/null +++ b/test/jest/secureOutlet.test.tsx @@ -0,0 +1,319 @@ +/*! + * Copyright (c) 2017-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. + */ + +import * as React from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { OktaAuth, AuthState } from '@okta/okta-auth-js'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { OktaContext, Security } from '@okta/okta-react'; +import { SecureOutlet } from '@okta/okta-react/react-router-6'; +import { MemoryRouter, Route, Routes } from 'react-router-dom6'; +import { SecurityProps } from '../../src/Security'; +import { ErrorComponent } from '../../src/OktaError'; + +jest.mock('react-router-dom', () => jest.requireActual('react-router-dom6')); + +describe('', () => { + let oktaAuth: OktaAuth; + let authState: AuthState | null; + let mockProps: SecurityProps; + const restoreOriginalUri = async (_: OktaAuth, url: string) => { + location.href = url; + }; + + beforeEach(() => { + authState = null; + oktaAuth = { + options: {}, + authStateManager: { + getAuthState: jest.fn().mockImplementation(() => authState), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + updateAuthState: jest.fn(), + }, + isLoginRedirect: jest.fn().mockImplementation(() => false), + handleLoginRedirect: jest.fn(), + signInWithRedirect: jest.fn(), + setOriginalUri: jest.fn(), + start: jest.fn(), + } as any as OktaAuth; + mockProps = { + oktaAuth, + restoreOriginalUri + }; + }); + + describe('With changing authState', () => { + let emitAuthState: () => void | undefined; + + beforeEach(() => { + oktaAuth.authStateManager.subscribe = (cb) => { + emitAuthState = () => { + act(cb.bind(null, authState as AuthState)); + }; + }; + }); + + function updateAuthState(newProps: Record | null = {}) { + authState = Object.assign({}, authState || {}, newProps); + emitAuthState(); + } + + it('calls login() only once until user is authenticated', () => { + authState = { + isAuthenticated: false + } + + mount( + + + + ); + expect(oktaAuth.signInWithRedirect).toHaveBeenCalledTimes(1); + (oktaAuth.signInWithRedirect as jest.Mock).mockClear(); + + updateAuthState(null); + expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); + + updateAuthState({}); + expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); + + updateAuthState({ isAuthenticated: true }); + expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); + + // If the state returns to unauthenticated, the SecureOutlet should still work + updateAuthState({ isAuthenticated: false }); + expect(oktaAuth.signInWithRedirect).toHaveBeenCalledTimes(1); + }); + }); + + describe('isAuthenticated: true', () => { + beforeEach(() => { + authState = { + isAuthenticated: true + }; + }); + + it('will render wrapped route component', () => { + const MyComponent = function() { return
hello world
; }; + const wrapper = mount( + + + + }> + } /> + + + + + ); + expect(wrapper.find(MyComponent).html()).toBe('
hello world
'); + }); + }); + + describe('isAuthenticated: false', () => { + beforeEach(() => { + authState = { + isAuthenticated: false + }; + }); + + it('will not render wrapped route component', () => { + const MyComponent = function() { return
hello world
; }; + const wrapper = mount( + + + + }> + } /> + + + + + ); + expect(wrapper.find(MyComponent).length).toBe(0); + }); + + describe('authState is not null', () => { + beforeEach(() => { + authState = {}; + }); + + it('calls signInWithRedirect() if onAuthRequired is not provided', () => { + mount( + + + + ); + expect(oktaAuth.setOriginalUri).toHaveBeenCalled(); + expect(oktaAuth.signInWithRedirect).toHaveBeenCalled(); + }); + + it('calls onAuthRequired if provided from Security', () => { + const onAuthRequired = jest.fn(); + mount( + + + + ); + expect(oktaAuth.setOriginalUri).toHaveBeenCalled(); + expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); + expect(onAuthRequired).toHaveBeenCalledWith(oktaAuth); + }); + + it('calls onAuthRequired from SecureOutlet if provide from both Security and SecureOutlet', () => { + const onAuthRequired1 = jest.fn(); + const onAuthRequired2 = jest.fn(); + mount( + + + + ); + expect(oktaAuth.setOriginalUri).toHaveBeenCalled(); + expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); + expect(onAuthRequired1).not.toHaveBeenCalled(); + expect(onAuthRequired2).toHaveBeenCalledWith(oktaAuth); + }); + }); + + describe('authState is null', () => { + beforeEach(() => { + authState = null; + }); + + it('does not call signInWithRedirect()', () => { + mount( + + + + ); + expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); + }); + }); + }); + + describe('Error handling', () => { + let container: HTMLElement | null = null; + beforeEach(() => { + // setup a DOM element as a render target + container = document.createElement('div'); + document.body.appendChild(container); + + authState = { + isAuthenticated: false + }; + + oktaAuth.setOriginalUri = jest.fn().mockImplementation(() => { + throw new Error(`DOMException: Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.`); + }); + }); + + afterEach(() => { + // cleanup on exiting + unmountComponentAtNode(container as Element); + container?.remove(); + container = null; + }); + + it('shows error with default OktaError component', async () => { + await act(async () => { + render( + + + , + container + ); + }); + expect(container?.innerHTML).toBe('

Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.

'); + }); + + it('shows error with provided custom error component', async () => { + const CustomErrorComponent: ErrorComponent = ({ error }) => { + return
Custom Error: {error.message}
; + }; + await act(async () => { + render( + + + , + container + ); + }); + expect(container?.innerHTML).toBe('
Custom Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.
'); + }); + }); + + describe('shows loading', () => { + let container: HTMLElement | null = null; + beforeEach(() => { + // setup a DOM element as a render target + container = document.createElement('div'); + document.body.appendChild(container); + + authState = { + isAuthenticated: false + }; + }); + + afterEach(() => { + // cleanup on exiting + unmountComponentAtNode(container as Element); + container?.remove(); + container = null; + }); + + it('does not render loading by default', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toBe(''); + }); + + it('custom loading element can be passed to render during loading', () => { + const MyLoadingElement = (

loading...

); + + const wrapper = mount( + + + + ); + expect(wrapper.text()).toBe('loading...'); + }); + + it('does not render loading element on error', async () => { + oktaAuth.setOriginalUri = jest.fn().mockImplementation(() => { + throw new Error('oh drat!'); + }); + const MyLoadingElement = (

loading...

); + + await act(async () => { + render( + + + , + container + ); + }); + expect(container?.innerHTML).toBe('

Error: oh drat!

'); + }); + }); + +}); \ No newline at end of file diff --git a/test/jest/secureOutletWithRR5.test.tsx b/test/jest/secureOutletWithRR5.test.tsx new file mode 100644 index 00000000..23f0449d --- /dev/null +++ b/test/jest/secureOutletWithRR5.test.tsx @@ -0,0 +1,82 @@ +/*! + * Copyright (c) 2017-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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as React from 'react'; +import { act } from 'react-dom/test-utils'; +import { render } from 'react-dom'; +import { OktaAuth, AuthState } from '@okta/okta-auth-js'; +import { SecureOutlet } from '@okta/okta-react/react-router-6'; +import { MemoryRouter } from 'react-router-dom6'; +import { Security } from '@okta/okta-react'; +import { SecurityProps } from '../../src/Security'; +import ErrorBoundary from './support/ErrorBoundary'; + +jest.mock('react-router-dom', () => jest.requireActual('react-router-dom')); + +describe('', () => { + describe('with react-router-dom v5', () => { + let oktaAuth: OktaAuth; + let authState: AuthState | null; + let mockProps: SecurityProps; + const restoreOriginalUri = async (_: OktaAuth, url: string) => { + location.href = url; + }; + + beforeEach(() => { + // prevents logging error to console + // eslint-disable-next-line @typescript-eslint/no-empty-function + console.error = (()=>{}); // noop + + authState = null; + oktaAuth = { + options: {}, + authStateManager: { + getAuthState: jest.fn().mockImplementation(() => authState), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + updateAuthState: jest.fn(), + }, + isLoginRedirect: jest.fn().mockImplementation(() => false), + handleLoginRedirect: jest.fn(), + signInWithRedirect: jest.fn(), + setOriginalUri: jest.fn(), + start: jest.fn(), + } as any as OktaAuth; + mockProps = { + oktaAuth, + restoreOriginalUri + }; + }); + + it('throws unsupported error', async () => { + authState = { + isAuthenticated: true + }; + const container = document.createElement('div'); + await act(async () => { + render( + + + + + + + , + container + ); + }); + expect(container.innerHTML).toBe('

AuthSdkError: Unsupported: SecureOutlet only works with react-router-dom v6 or any router library with compatible APIs. See examples under the "samples" folder for how to implement your own custom SecureRoute Component.

'); + }) + }); +}); diff --git a/test/jest/secureRoute.test.tsx b/test/jest/secureRoute.test.tsx index 0f108bf6..c2490dd6 100644 --- a/test/jest/secureRoute.test.tsx +++ b/test/jest/secureRoute.test.tsx @@ -15,15 +15,19 @@ import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { render, unmountComponentAtNode } from 'react-dom'; import { MemoryRouter, Route, RouteProps } from 'react-router-dom'; -import SecureRoute from '../../src/SecureRoute'; -import Security from '../../src/Security'; -import OktaContext from '../../src/OktaContext'; +import { OktaAuth, AuthState } from '@okta/okta-auth-js'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; +import { Security, OktaContext } from '@okta/okta-react'; +import { SecurityProps } from '../../src/Security'; +import { ErrorComponent } from '../../src/OktaError'; + +jest.mock('react-router-dom', () => jest.requireActual('react-router-dom')); describe('', () => { - let oktaAuth; - let authState; - let mockProps; - const restoreOriginalUri = async (_, url) => { + let oktaAuth: OktaAuth; + let authState: AuthState | null; + let mockProps: SecurityProps; + const restoreOriginalUri = async (_: OktaAuth, url: string) => { location.href = url; }; @@ -42,7 +46,7 @@ describe('', () => { signInWithRedirect: jest.fn(), setOriginalUri: jest.fn(), start: jest.fn(), - }; + } as any as OktaAuth; mockProps = { oktaAuth, restoreOriginalUri @@ -50,17 +54,17 @@ describe('', () => { }); describe('With changing authState', () => { - let emitAuthState; + let emitAuthState: () => void | undefined; beforeEach(() => { oktaAuth.authStateManager.subscribe = (cb) => { emitAuthState = () => { - act(cb.bind(null, authState)); + act(cb.bind(null, authState as AuthState)); }; }; }); - function updateAuthState(newProps = {}) { + function updateAuthState(newProps: Record | null = {}) { authState = Object.assign({}, authState || {}, newProps); emitAuthState(); } @@ -78,7 +82,7 @@ describe('', () => { ); expect(oktaAuth.signInWithRedirect).toHaveBeenCalledTimes(1); - oktaAuth.signInWithRedirect.mockClear(); + (oktaAuth.signInWithRedirect as jest.Mock).mockClear(); updateAuthState(null); expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled(); @@ -358,8 +362,10 @@ describe('', () => { }); it('should pass react-router props to an component', () => { - authState.isAuthenticated = true; - const MyComponent = function(props) { return
{ props.history ? 'has history' : 'lacks history'}
; }; + (authState as AuthState).isAuthenticated = true; + const MyComponent = function(props: Record) { + return
{ props.history ? 'has history' : 'lacks history'}
; + }; const wrapper = mount( @@ -374,8 +380,10 @@ describe('', () => { }); it('should pass react-router props to a render call', () => { - authState.isAuthenticated = true; - const MyComponent = function(props) { return
{ props.history ? 'has history' : 'lacks history'}
; }; + (authState as AuthState).isAuthenticated = true; + const MyComponent = function(props: Record) { + return
{ props.history ? 'has history' : 'lacks history'}
; + }; const wrapper = mount( @@ -390,8 +398,10 @@ describe('', () => { }); it('should pass props using the "render" prop', () => { - authState.isAuthenticated = true; - const MyComponent = function(props) { return
{ props.someProp ? 'has someProp' : 'lacks someProp'}
; }; + (authState as AuthState).isAuthenticated = true; + const MyComponent = function(props: Record) { + return
{ props.someProp ? 'has someProp' : 'lacks someProp'}
; + }; const wrapper = mount( @@ -408,7 +418,7 @@ describe('', () => { }); describe('Error handling', () => { - let container = null; + let container: HTMLElement | null = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement('div'); @@ -425,8 +435,8 @@ describe('', () => { afterEach(() => { // cleanup on exiting - unmountComponentAtNode(container); - container.remove(); + unmountComponentAtNode(container as Element); + container?.remove(); container = null; }); @@ -444,11 +454,11 @@ describe('', () => { container ); }); - expect(container.innerHTML).toBe('

Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.

'); + expect(container?.innerHTML).toBe('

Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.

'); }); it('shows error with provided custom error component', async () => { - const CustomErrorComponent = ({ error }) => { + const CustomErrorComponent: ErrorComponent = ({ error }) => { return
Custom Error: {error.message}
; }; await act(async () => { @@ -464,7 +474,7 @@ describe('', () => { container ); }); - expect(container.innerHTML).toBe('
Custom Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.
'); + expect(container?.innerHTML).toBe('
Custom Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.
'); }); }); }); diff --git a/test/jest/secureRouteWithRR6.test.tsx b/test/jest/secureRouteWithRR6.test.tsx new file mode 100644 index 00000000..25333fb7 --- /dev/null +++ b/test/jest/secureRouteWithRR6.test.tsx @@ -0,0 +1,79 @@ +/*! + * Copyright (c) 2017-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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as React from 'react'; +import { act } from 'react-dom/test-utils'; +import { render } from 'react-dom'; +import { OktaAuth, AuthState } from '@okta/okta-auth-js'; +import { SecureRoute } from '@okta/okta-react/react-router-5'; +import { MemoryRouter } from 'react-router-dom6'; +import { Security } from '@okta/okta-react'; +import { SecurityProps } from '../../src/Security'; +import ErrorBoundary from './support/ErrorBoundary'; + +jest.mock('react-router-dom', () => jest.requireActual('react-router-dom6')); + +describe('', () => { + describe('with react-router-dom v6', () => { + let oktaAuth: OktaAuth; + let authState: AuthState | null; + let mockProps: SecurityProps; + const restoreOriginalUri = async (_: OktaAuth, url: string) => { + location.href = url; + }; + + beforeEach(() => { + // prevents logging error to console + // eslint-disable-next-line @typescript-eslint/no-empty-function + console.error = (()=>{}); // noop + + authState = null; + oktaAuth = { + options: {}, + authStateManager: { + getAuthState: jest.fn().mockImplementation(() => authState), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + updateAuthState: jest.fn(), + }, + isLoginRedirect: jest.fn().mockImplementation(() => false), + handleLoginRedirect: jest.fn(), + signInWithRedirect: jest.fn(), + setOriginalUri: jest.fn(), + start: jest.fn(), + } as any as OktaAuth; + mockProps = { + oktaAuth, + restoreOriginalUri + }; + }); + + it('throws unsupported error', async () => { + const container = document.createElement('div'); + await act(async () => { + render( + + + + + + + , + container + ); + }); + expect(container.innerHTML).toBe('

AuthSdkError: Unsupported: SecureRoute only works with react-router-dom v5 or any router library with compatible APIs. See examples under the "samples" folder for how to implement your own custom SecureRoute Component.

'); + }) + }); +}); diff --git a/test/jest/security.test.tsx b/test/jest/security.test.tsx index a916113a..56ddaeda 100644 --- a/test/jest/security.test.tsx +++ b/test/jest/security.test.tsx @@ -14,9 +14,9 @@ import * as React from 'react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; -import Security from '../../src/Security'; -import { useOktaAuth } from '../../src/OktaContext'; import { AuthState, OktaAuth } from '@okta/okta-auth-js'; +import { SecurityProps } from '../../src/Security'; +import { IOktaContext } from '../../src/OktaContext'; declare global { let SKIP_VERSION_CHECK: any; @@ -24,13 +24,22 @@ declare global { console.warn = jest.fn(); +/* Forces Jest to use same version of React to enable fresh module state via isolateModulesAsync() call in beforeEach(). +Otherwise, React raises "Invalid hook call" error because of multiple copies of React, see: https://github.com/jestjs/jest/issues/11471#issuecomment-851266333 */ +jest.mock('react', () => jest.requireActual('react')); + describe('', () => { let oktaAuth: OktaAuth; let initialAuthState: AuthState | null; + let Security: React.FC; + let useOktaAuth: () => IOktaContext; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars const restoreOriginalUri = async (_: OktaAuth, url: string) => { - location.href = url; + // leaving empty, doesn't affect tests, was causing jsdom error (location.href is not supported) + // location.href = url; }; - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); initialAuthState = { @@ -53,6 +62,13 @@ describe('', () => { stop: jest.fn(), isLoginRedirect: jest.fn().mockImplementation(() => false), } as any; + + // Dynamically import Security and useOktaAuth before each test to refresh the modules' states + // Specifically used to reset the global restoreOriginalUriOverridden variable in Security.tsx between tests + await jest.isolateModulesAsync(async () => { + Security = (await import('../../src/Security')).default; + useOktaAuth = (await import('../../src/OktaContext')).useOktaAuth; + }); }); it('adds an environmemnt to oktaAuth\'s _oktaUserAgent', () => { @@ -136,14 +152,58 @@ describe('', () => { }); }); - it('should set default restoreOriginalUri callback in oktaAuth.options', () => { - oktaAuth.options = {}; - const mockProps = { - oktaAuth, - restoreOriginalUri - }; - mount(); - expect(oktaAuth.options.restoreOriginalUri).toBeDefined(); + describe('restoreOriginalUri', () => { + it('should set default restoreOriginalUri callback in oktaAuth.options', () => { + oktaAuth.options = {}; + const mockProps = { + oktaAuth, + restoreOriginalUri + }; + mount(); + expect(oktaAuth.options.restoreOriginalUri).toBeDefined(); + }); + + it('should only log warning of restoreOriginalUri option once', () => { + oktaAuth.options = { + restoreOriginalUri + }; + const mockProps = { + oktaAuth, + restoreOriginalUri + }; + const warning = 'Two custom restoreOriginalUri callbacks are detected. The one from the OktaAuth configuration will be overridden by the provided restoreOriginalUri prop from the Security component.'; + const spy = jest.spyOn(console, 'warn'); + const wrapper = mount(); + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith(warning); + spy.mockClear(); + wrapper.setProps({restoreOriginalUri: 'foo'}); // forces rerender + expect(spy).toBeCalledTimes(0); + }); + + it('should await the resulting Promise when a fn returning a Promise is provided', async () => { + oktaAuth.options = {}; + + let hasResolved = false; + const restoreSpy = jest.fn().mockImplementation(() => { + return new Promise(resolve => { + // adds small sleep so non-awaited promises will "fallthrough" + // and the test will fail, unless it awaits for the sleep duration + // (meaning the resulting promise was awaited) + setTimeout(() => { + hasResolved = true; + resolve('foo'); + }, 500); + }); + }); + + mount(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await oktaAuth.options.restoreOriginalUri!(oktaAuth, 'foo'); + expect(hasResolved).toEqual(true); + expect(restoreSpy).toHaveBeenCalledTimes(1); + expect(restoreSpy).toHaveBeenCalledWith(oktaAuth, 'foo'); + }); }); it('gets initial state from oktaAuth and exposes it on the context', () => { @@ -394,22 +454,4 @@ describe('', () => { expect(wrapper.find(Security).html()).toBe('

AuthSdkError: No restoreOriginalUri callback passed to Security Component.

'); }); }); - - it('should only log warning of restoreOriginalUri option once', () => { - oktaAuth.options = { - restoreOriginalUri - }; - const mockProps = { - oktaAuth, - restoreOriginalUri - }; - const warning = 'Two custom restoreOriginalUri callbacks are detected. The one from the OktaAuth configuration will be overridden by the provided restoreOriginalUri prop from the Security component.'; - const spy = jest.spyOn(console, 'warn'); - const wrapper = mount(); - expect(spy).toBeCalledTimes(1); - expect(spy).toBeCalledWith(warning); - spy.mockClear(); - wrapper.setProps({restoreOriginalUri: 'foo'}); // forces rerender - expect(spy).toBeCalledTimes(0); - }); }); diff --git a/test/jest/support/ErrorBoundary.tsx b/test/jest/support/ErrorBoundary.tsx new file mode 100644 index 00000000..0d12c956 --- /dev/null +++ b/test/jest/support/ErrorBoundary.tsx @@ -0,0 +1,42 @@ +/*! + * Copyright (c) 2017-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. + */ + +import * as React from 'react'; +import { AuthSdkError } from '@okta/okta-auth-js'; + +interface ErrorBoundaryState { + error: AuthSdkError | null; +} + +class ErrorBoundary extends React.Component>, ErrorBoundaryState> { + constructor(props: Record) { + super(props); + this.state = { + error: null + }; + } + + componentDidCatch(error: AuthSdkError): void { + this.setState({ error: error }); + } + + render(): React.ReactNode { + if (this.state.error) { + // You can render any custom fallback UI + return

{ this.state.error.toString() }

; + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/test/jest/tsconfig.json b/test/jest/tsconfig.json index cc9c5454..390d5e3a 100644 --- a/test/jest/tsconfig.json +++ b/test/jest/tsconfig.json @@ -4,6 +4,12 @@ "jsx": "react", "esModuleInterop": true, "resolveJsonModule": true, - "strict": true + "strict": true, + "paths": { + "react-router-dom6": ["./node_modules/react-router-dom6/dist/index.d.ts"], + "@okta/okta-react": ["../../src/index.ts"], + "@okta/okta-react/react-router-5": ["../../src/react-router-5.ts"], + "@okta/okta-react/react-router-6": ["../../src/react-router-6.ts"], + } } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2dda3135..825479b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1205,6 +1205,13 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" +"@babel/runtime@7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" + integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.0", "@babel/runtime@^7.18.9", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" @@ -1720,7 +1727,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@okta/okta-auth-js@*", "@okta/okta-auth-js@^7.0.0": +"@okta/okta-auth-js@*": version "7.0.0" resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-7.0.0.tgz#d83acb76394ebc7d019d922f0e319fe6655c41ac" integrity sha512-tF+OiaAHNHc5ACKUR5lynX7LQyw9+pqKj8hzSY+E6OCKucYudjMg87AdCdyBxpFgxrirRlAFYv08sM1wwn0Eeg== @@ -1765,6 +1772,28 @@ webcrypto-shim "^0.1.5" xhr2 "0.1.3" +"@okta/okta-auth-js@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@okta/okta-auth-js/-/okta-auth-js-7.7.0.tgz#daac09294316a69d996a33232eb25032d1b85d70" + integrity sha512-m+WlI9TJ3J2uHI+W9Uc7zinE4CQLS2JC6AQYPJ0KHxaVE5lwPDLFleapPNfNWzYGr/30GV7oBzJMU+8+UQEsPA== + dependencies: + "@babel/runtime" "^7.12.5" + "@peculiar/webcrypto" "^1.4.0" + Base64 "1.1.0" + atob "^2.1.2" + broadcast-channel "~5.3.0" + btoa "^1.2.1" + core-js "^3.6.5" + cross-fetch "^3.1.5" + fast-text-encoding "^1.0.6" + js-cookie "^3.0.1" + jsonpath-plus "^6.0.1" + node-cache "^5.1.2" + p-cancelable "^2.0.0" + tiny-emitter "1.1.0" + webcrypto-shim "^0.1.5" + xhr2 "0.1.3" + "@okta/okta-signin-widget@^6.7.1": version "6.9.0" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-all/@okta/okta-signin-widget/-/@okta/okta-signin-widget-6.9.0.tgz#252d939525a422580c51e549a6e4ed4cd20f4765" @@ -1830,6 +1859,11 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" +"@remix-run/router@1.15.1": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.1.tgz#221fd31a65186b9bc027b74573485fb3226dff7f" + integrity sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w== + "@rollup/plugin-babel@^5.2.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" @@ -3266,6 +3300,16 @@ broadcast-channel@~4.17.0: rimraf "3.0.2" unload "2.3.1" +broadcast-channel@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-5.3.0.tgz#9d9e55fb8db2a1dbbe436ae6d51382a354e76fc3" + integrity sha512-0PmDYc/iUGZ4QbnCnV7u+WleygiS1bZ4oV6t4rANXYtSgEFtGhB5jimJPLOVpPtce61FVxrH8CYylfO5g7OLKw== + dependencies: + "@babel/runtime" "7.22.10" + oblivious-set "1.1.1" + p-queue "6.6.2" + unload "2.4.1" + browserslist@^4.20.2, browserslist@^4.21.9: version "4.21.10" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" @@ -4903,6 +4947,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-text-encoding@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + fastq@^1.6.0: version "1.13.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" @@ -5199,40 +5248,15 @@ glob-parent@^5.1.2, glob-parent@^6.0.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.3" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0, glob@^8.0.3, glob@^9.3.5, glob@~7.1.1: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== dependencies: fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.0.3: - version "8.1.0" - resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -glob@~7.1.1: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" globals@^11.1.0: version "11.12.0" @@ -5569,15 +5593,7 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflight@^1.0.4: - version "1.0.6" - resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6900,6 +6916,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +"lru-cache@^9.1.1 || ^10.0.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lz-string@^1.5.0: version "1.5.0" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -7019,7 +7040,7 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.12.1" tiny-warning "^1.0.3" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -7040,6 +7061,13 @@ minimatch@^6.0.4: dependencies: brace-expansion "^2.0.1" +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + minimatch@~3.0.2: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" @@ -7052,6 +7080,16 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" @@ -7263,7 +7301,7 @@ oblivious-set@1.1.1: resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.1.1.tgz#d9d38e9491d51f27a5c3ec1681d2ba40aa81e98b" integrity sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -7445,11 +7483,6 @@ path-exists@^4.0.0: resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -7460,6 +7493,14 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.6.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" @@ -7898,6 +7939,14 @@ react-refresh@^0.13.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.13.0.tgz#cbd01a4482a177a5da8d44c9755ebb1f26d5a1c1" integrity sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg== +"react-router-dom6@npm:react-router-dom@^6.22.0": + version "6.22.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.1.tgz#cfa109d4b6b0a4d00bac179bc0ad2a6469455282" + integrity sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw== + dependencies: + "@remix-run/router" "1.15.1" + react-router "6.22.1" + react-router-dom@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" @@ -7979,6 +8028,13 @@ react-router@6.2.1: dependencies: history "^5.2.0" +react-router@6.22.1: + version "6.22.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.1.tgz#a5ff849bfe709438f7e139421bb28138209662c7" + integrity sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ== + dependencies: + "@remix-run/router" "1.15.1" + react-router@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" @@ -9201,6 +9257,11 @@ unload@2.3.1: "@babel/runtime" "^7.6.2" detect-node "2.1.0" +unload@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.4.1.tgz#b0c5b7fb44e17fcbf50dcb8fb53929c59dd226a5" + integrity sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw== + update-browserslist-db@^1.0.11, update-browserslist-db@^1.0.5: version "1.0.11" resolved "https://artifacts.aue1e.internal:443/artifactory/api/npm/npm-okta-master/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"