diff --git a/CHANGELOG.md b/CHANGELOG.md index 775292f8..372edb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 6.3.0 + +### Features + +- [#172](https://github.com/okta/okta-react/pull/172) Adds `errorComponent` prop to `SecureRoute` to handle internal `handleLogin` related errors + # 6.2.0 ### Other diff --git a/README.md b/README.md index 7c4c9001..0c7202f5 100644 --- a/README.md +++ b/README.md @@ -466,7 +466,15 @@ 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. +#### onAuthRequired + `SecureRoute` accepts `onAuthRequired` as an optional prop, it overrides [onAuthRequired](#onauthrequired) from the [Security](#security) component if exists. + +#### errorComponent + +`SecureRoute` 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. + +#### `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. diff --git a/src/SecureRoute.tsx b/src/SecureRoute.tsx index f3b9ea67..e4289401 100644 --- a/src/SecureRoute.tsx +++ b/src/SecureRoute.tsx @@ -14,16 +14,21 @@ import * as React from 'react'; import { useOktaAuth, OnAuthRequiredFunction } from './OktaContext'; import { Route, useRouteMatch, RouteProps } from 'react-router-dom'; import { toRelativeUrl } from '@okta/okta-auth-js'; +import OktaError from './OktaError'; const SecureRoute: React.FC<{ onAuthRequired?: OnAuthRequiredFunction; + errorComponent?: React.ComponentType<{ error: Error }>; } & RouteProps & React.HTMLAttributes> = ({ - onAuthRequired, + onAuthRequired, + errorComponent, ...routeProps }) => { const { oktaAuth, authState, _onAuthRequired } = useOktaAuth(); const match = useRouteMatch(routeProps); const pendingLogin = React.useRef(false); + const [handleLoginError, setHandleLoginError] = React.useState(null); + const ErrorReporter = errorComponent || OktaError; React.useEffect(() => { const handleLogin = async () => { @@ -59,18 +64,23 @@ const SecureRoute: React.FC<{ // Start login if app has decided it is not logged in and there is no pending signin if(!authState.isAuthenticated) { - handleLogin(); + handleLogin().catch(err => { + setHandleLoginError(err as Error); + }); } }, [ - !!authState, - authState ? authState.isAuthenticated : null, + authState, oktaAuth, match, onAuthRequired, _onAuthRequired ]); + if (handleLoginError) { + return ; + } + if (!authState || !authState.isAuthenticated) { return null; } diff --git a/src/Security.tsx b/src/Security.tsx index 9b184a37..8bf151e6 100644 --- a/src/Security.tsx +++ b/src/Security.tsx @@ -69,11 +69,7 @@ const Security: React.FC<{ oktaAuth.authStateManager.subscribe(handler); // Trigger an initial change event to make sure authState is latest - if (!oktaAuth.isLoginRedirect()) { - // Calculates initial auth state and fires change event for listeners - // Also starts the token auto-renew service - oktaAuth.start(); - } + oktaAuth.start(); return () => { oktaAuth.authStateManager.unsubscribe(handler); diff --git a/test/jest/secureRoute.test.tsx b/test/jest/secureRoute.test.tsx index 693117a9..0f108bf6 100644 --- a/test/jest/secureRoute.test.tsx +++ b/test/jest/secureRoute.test.tsx @@ -13,9 +13,11 @@ import * as React from 'react'; 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'; describe('', () => { let oktaAuth; @@ -404,4 +406,65 @@ describe('', () => { }); }); + + describe('Error handling', () => { + let container = 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); + 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 = ({ 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.
'); + }); + }); }); diff --git a/test/jest/security.test.tsx b/test/jest/security.test.tsx index 34ef4162..0a6e279a 100644 --- a/test/jest/security.test.tsx +++ b/test/jest/security.test.tsx @@ -43,7 +43,6 @@ describe('', () => { subscribe: jest.fn(), unsubscribe: jest.fn(), }, - isLoginRedirect: jest.fn().mockImplementation(() => false), start: jest.fn(), stop: jest.fn(), }; @@ -187,20 +186,6 @@ describe('', () => { expect(MyComponent).toHaveBeenCalledTimes(2); }); - it('should not call start when in login redirect state', () => { - oktaAuth.isLoginRedirect = jest.fn().mockImplementation(() => true); - const mockProps = { - oktaAuth, - restoreOriginalUri - }; - mount( - - - - ); - expect(oktaAuth.start).not.toHaveBeenCalled(); - }); - it('subscribes to "authStateChange" and updates the context', () => { const mockAuthStates = [ initialAuthState,