Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add state option to onAuthRequired callback - OKTA-422339 #166

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

- [#166](https://github.com/okta/okta-react/pull/166) Adds options with `OnAuthRequiredState` to `onAuthRequired` callback, so custom authentication approach can be provided based on when authState is updated

# 6.2.0

### Other
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,12 @@ export default MessageList = () => {

#### 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 and an options object as the second 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).

- options:
- state: The state that when `authState` is updated. It can be either:
- `OnAuthRequiredState.Initialized`: when `authState` is updated during app initial load
- `OnAuthRequiredState.Updated`: when `authState` is updated with tokens auto renew process

#### Example

Expand All @@ -410,10 +415,13 @@ const oktaAuth = new OktaAuth({
export default App = () => {
const history = useHistory();

const customAuthHandler = (oktaAuth) => {
// Redirect to the /login page that has a CustomLoginComponent
// This example is specific to React-Router
history.push('/login');
const customAuthHandler = (oktaAuth, options) => {
// Provide custom auth approach based on option.state
if (option.state === OnAuthRequiredState.Initialized) {
// ...
} else if (option.state === OnAuthRequiredState.Updated) {
// ...
}
};

const restoreOriginalUri = async (_oktaAuth, originalUri) => {
Expand Down
12 changes: 12 additions & 0 deletions generated/samples/custom-login/config-overrides.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/custom-login/env/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions generated/samples/custom-login/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/doc-direct-auth/config-overrides.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/doc-direct-auth/env/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion generated/samples/doc-direct-auth/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/doc-embedded-widget/config-overrides.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/doc-embedded-widget/env/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions generated/samples/doc-embedded-widget/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/okta-hosted-login/config-overrides.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions generated/samples/okta-hosted-login/env/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion generated/samples/okta-hosted-login/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion src/OktaContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@
import * as React from 'react';
import { AuthState, OktaAuth } from '@okta/okta-auth-js';

export type OnAuthRequiredFunction = (oktaAuth: OktaAuth) => Promise<void> | void;
export enum OnAuthRequiredState {
Initialized = 'INITIALIZED',
Updated = 'UPDATED'
}

export type OnAuthRequiredFunction = (
oktaAuth: OktaAuth,
options: { state: OnAuthRequiredState }
) => Promise<void> | void;
export type OnAuthResumeFunction = () => void;

export type RestoreOriginalUriFunction = (oktaAuth: OktaAuth, originalUri: string) => Promise<void> | void;
Expand All @@ -21,6 +29,7 @@ export interface IOktaContext {
oktaAuth: OktaAuth;
authState: AuthState | null;
_onAuthRequired?: OnAuthRequiredFunction;
_onAuthRequiredState: OnAuthRequiredState | null;
}

const OktaContext = React.createContext<IOktaContext | null>(null);
Expand Down
11 changes: 8 additions & 3 deletions src/SecureRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import * as React from 'react';
import { useOktaAuth, OnAuthRequiredFunction } from './OktaContext';
import { useOktaAuth, OnAuthRequiredFunction, OnAuthRequiredState } from './OktaContext';
import { Route, useRouteMatch, RouteProps } from 'react-router-dom';
import { toRelativeUrl } from '@okta/okta-auth-js';

Expand All @@ -21,7 +21,12 @@ const SecureRoute: React.FC<{
onAuthRequired,
...routeProps
}) => {
const { oktaAuth, authState, _onAuthRequired } = useOktaAuth();
const {
oktaAuth,
authState,
_onAuthRequired,
_onAuthRequiredState
} = useOktaAuth();
const match = useRouteMatch(routeProps);
const pendingLogin = React.useRef(false);

Expand All @@ -37,7 +42,7 @@ const SecureRoute: React.FC<{
oktaAuth.setOriginalUri(originalUri);
const onAuthRequiredFn = onAuthRequired || _onAuthRequired;
if (onAuthRequiredFn) {
await onAuthRequiredFn(oktaAuth);
await onAuthRequiredFn(oktaAuth, { state: _onAuthRequiredState as OnAuthRequiredState });
} else {
await oktaAuth.signInWithRedirect();
}
Expand Down
21 changes: 13 additions & 8 deletions src/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

import * as React from 'react';
import { AuthSdkError, AuthState, OktaAuth } from '@okta/okta-auth-js';
import OktaContext, { OnAuthRequiredFunction, RestoreOriginalUriFunction } from './OktaContext';
import OktaContext, {
OnAuthRequiredFunction,
RestoreOriginalUriFunction,
OnAuthRequiredState
} from './OktaContext';
import OktaError from './OktaError';

const Security: React.FC<{
Expand Down Expand Up @@ -41,6 +45,7 @@ const Security: React.FC<{
const majorVersion = oktaAuthVersion?.split('.')[0];
return majorVersion;
});
const onAuthRequiredState = React.useRef<OnAuthRequiredState | null>(null);

React.useEffect(() => {
if (!oktaAuth || !restoreOriginalUri) {
Expand All @@ -64,16 +69,15 @@ const Security: React.FC<{

// Update Security provider with latest authState
const handler = (authState: AuthState) => {
onAuthRequiredState.current = onAuthRequiredState.current === OnAuthRequiredState.Initialized
? OnAuthRequiredState.Updated
: OnAuthRequiredState.Initialized;
setAuthState(authState);
};
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();
}
// Start services
oktaAuth.start();

return () => {
oktaAuth.authStateManager.unsubscribe(handler);
Expand Down Expand Up @@ -102,7 +106,8 @@ const Security: React.FC<{
<OktaContext.Provider value={{
oktaAuth,
authState,
_onAuthRequired: onAuthRequired
_onAuthRequired: onAuthRequired,
_onAuthRequiredState: onAuthRequiredState.current
}}>
{children}
</OktaContext.Provider>
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/harness/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.8",
"private": true,
"dependencies": {
"@okta/okta-auth-js": "^5.4.0",
"@okta/okta-auth-js": "^5.5.0",
"@okta/okta-react": "*",
"@types/node": "^14.14.7",
"@types/react": "^16.9.56",
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
Expand Down
10 changes: 8 additions & 2 deletions test/jest/secureRoute.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter, Route, RouteProps } from 'react-router-dom';
import SecureRoute from '../../src/SecureRoute';
import Security from '../../src/Security';
import { OnAuthRequiredState } from '../../src/OktaContext';

describe('<SecureRoute />', () => {
let oktaAuth;
Expand Down Expand Up @@ -215,6 +216,9 @@ describe('<SecureRoute />', () => {

it('calls onAuthRequired if provided from Security', () => {
const onAuthRequired = jest.fn();
jest.spyOn(React, 'useRef')
.mockReturnValueOnce({ current: OnAuthRequiredState.Initialized });

mount(
<MemoryRouter>
<Security {...mockProps} onAuthRequired={onAuthRequired}>
Expand All @@ -224,12 +228,14 @@ describe('<SecureRoute />', () => {
);
expect(oktaAuth.setOriginalUri).toHaveBeenCalled();
expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled();
expect(onAuthRequired).toHaveBeenCalledWith(oktaAuth);
expect(onAuthRequired).toHaveBeenCalledWith(oktaAuth, { state: OnAuthRequiredState.Initialized });
});

it('calls onAuthRequired from SecureRoute if provide from both Security and SecureRoute', () => {
const onAuthRequired1 = jest.fn();
const onAuthRequired2 = jest.fn();
jest.spyOn(React, 'useRef')
.mockReturnValueOnce({ current: OnAuthRequiredState.Initialized });
mount(
<MemoryRouter>
<Security {...mockProps} onAuthRequired={onAuthRequired1}>
Expand All @@ -240,7 +246,7 @@ describe('<SecureRoute />', () => {
expect(oktaAuth.setOriginalUri).toHaveBeenCalled();
expect(oktaAuth.signInWithRedirect).not.toHaveBeenCalled();
expect(onAuthRequired1).not.toHaveBeenCalled();
expect(onAuthRequired2).toHaveBeenCalledWith(oktaAuth);
expect(onAuthRequired2).toHaveBeenCalledWith(oktaAuth, { state: OnAuthRequiredState.Initialized });
});
});

Expand Down
Loading