Skip to content

Commit

Permalink
feat: add errorComponent props to SecureRoute
Browse files Browse the repository at this point in the history
OKTA-431793
<<<Jenkins Check-In of Tested SHA: 9e898e4 for eng_productivity_ci_bot_okta@okta.com>>>
Artifact: okta-react
Files changed count: 6
PR Link: "#172"
  • Loading branch information
shuowu authored and eng-prod-CI-bot-okta committed Nov 1, 2021
1 parent 8b75562 commit 5f059ef
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 24 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<SecureRoute>`. 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.

Expand Down
18 changes: 14 additions & 4 deletions src/SecureRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>> = ({
onAuthRequired,
onAuthRequired,
errorComponent,
...routeProps
}) => {
const { oktaAuth, authState, _onAuthRequired } = useOktaAuth();
const match = useRouteMatch(routeProps);
const pendingLogin = React.useRef(false);
const [handleLoginError, setHandleLoginError] = React.useState<Error | null>(null);
const ErrorReporter = errorComponent || OktaError;

React.useEffect(() => {
const handleLogin = async () => {
Expand Down Expand Up @@ -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 <ErrorReporter error={handleLoginError} />;
}

if (!authState || !authState.isAuthenticated) {
return null;
}
Expand Down
6 changes: 1 addition & 5 deletions src/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
63 changes: 63 additions & 0 deletions test/jest/secureRoute.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('<SecureRoute />', () => {
let oktaAuth;
Expand Down Expand Up @@ -404,4 +406,65 @@ describe('<SecureRoute />', () => {
});

});

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(
<MemoryRouter>
<OktaContext.Provider value={{
oktaAuth: oktaAuth,
authState
}}>
<SecureRoute path="/" />
</OktaContext.Provider>
</MemoryRouter>,
container
);
});
expect(container.innerHTML).toBe('<p>Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.</p>');
});

it('shows error with provided custom error component', async () => {
const CustomErrorComponent = ({ error }) => {
return <div>Custom Error: {error.message}</div>;
};
await act(async () => {
render(
<MemoryRouter>
<OktaContext.Provider value={{
oktaAuth: oktaAuth,
authState
}}>
<SecureRoute path="/" errorComponent={CustomErrorComponent} />
</OktaContext.Provider>
</MemoryRouter>,
container
);
});
expect(container.innerHTML).toBe('<div>Custom Error: DOMException: Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.</div>');
});
});
});
15 changes: 0 additions & 15 deletions test/jest/security.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ describe('<Security />', () => {
subscribe: jest.fn(),
unsubscribe: jest.fn(),
},
isLoginRedirect: jest.fn().mockImplementation(() => false),
start: jest.fn(),
stop: jest.fn(),
};
Expand Down Expand Up @@ -187,20 +186,6 @@ describe('<Security />', () => {
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(
<MemoryRouter>
<Security {...mockProps} />
</MemoryRouter>
);
expect(oktaAuth.start).not.toHaveBeenCalled();
});

it('subscribes to "authStateChange" and updates the context', () => {
const mockAuthStates = [
initialAuthState,
Expand Down

0 comments on commit 5f059ef

Please sign in to comment.