Skip to content

feat: onAuthExpired callback #161

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions src/OktaContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import { AuthState, OktaAuth } from '@okta/okta-auth-js';

export type OnAuthRequiredFunction = (oktaAuth: OktaAuth) => Promise<void> | void;
export type OnAuthResumeFunction = () => void;
export type OnAuthExpiredFunction = (oktaAuth: OktaAuth) => Promise<void> | void;

export type RestoreOriginalUriFunction = (oktaAuth: OktaAuth, originalUri: string) => Promise<void> | void;

export interface IOktaContext {
oktaAuth: OktaAuth;
authState: AuthState | null;
_onAuthRequired?: OnAuthRequiredFunction;
_onAuthExpired?: OnAuthExpiredFunction;
}

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

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

const SecureRoute: React.FC<{
onAuthRequired?: OnAuthRequiredFunction;
onAuthExpired?: OnAuthExpiredFunction;
} & RouteProps & React.HTMLAttributes<HTMLDivElement>> = ({
onAuthRequired,
onAuthRequired,
onAuthExpired,
...routeProps
}) => {
const { oktaAuth, authState, _onAuthRequired } = useOktaAuth();
const { oktaAuth, authState, _onAuthRequired, _onAuthExpired } = useOktaAuth();
const match = useRouteMatch(routeProps);
const pendingLogin = React.useRef(false);
const hasAuthenticated = React.useRef(false);

React.useEffect(() => {
const handleLogin = async () => {
if (pendingLogin.current) {
return;
}

pendingLogin.current = true;

// Save the current route before any redirection
const originalUri = toRelativeUrl(window.location.href, window.location.origin);
oktaAuth.setOriginalUri(originalUri);

// We have previously signed in
const hasExpired = hasAuthenticated.current === true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we are able to detect if it's triggered by initial load or future authState change, how about feeding an extra state object to the onAuthRequiredFn callback, then devs can code based on that?

onAuthRequired(oktaAuth, options /* probably not the best name */) 

In this way, we make the workaround possible with a minor change. Thoughts?

if (hasExpired) {
const onAuthExpiredFn = onAuthExpired || _onAuthExpired;
if (onAuthExpiredFn) {
await onAuthExpiredFn(oktaAuth);
} else {
// There is no default behavior.
// App should define an `onAuthExpired` handler to render some type of UI (for example a modal overlay) with a link to signin again.
}
return;
}

// First-time loading this route, do the auth flow
pendingLogin.current = true;
const onAuthRequiredFn = onAuthRequired || _onAuthRequired;
if (onAuthRequiredFn) {
await onAuthRequiredFn(oktaAuth);
Expand All @@ -54,24 +72,29 @@ const SecureRoute: React.FC<{

if (authState.isAuthenticated) {
pendingLogin.current = false;
hasAuthenticated.current = true;
return;
}

// Start login if app has decided it is not logged in and there is no pending signin
if(!authState.isAuthenticated) {
handleLogin();
}
// isAuthenticated is false
handleLogin();

}, [
!!authState,
authState ? authState.isAuthenticated : null,
oktaAuth,
match,
onAuthRequired,
_onAuthRequired
_onAuthRequired,
onAuthExpired,
_onAuthExpired
]);

if (!authState || !authState.isAuthenticated) {
// If the user has authenticated on this route (since component load), the component will be rendered.
// It is possible that the app's tokens have expired or been removed since that point in time.
// App should define an `onAuthExpired` handler to render some type of UI (for example a modal overlay) with a link to signin again.
const shouldRender = (authState && authState.isAuthenticated) || hasAuthenticated.current;
if (!shouldRender) {
return null;
}

Expand Down
9 changes: 6 additions & 3 deletions src/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@

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

const Security: React.FC<{
oktaAuth: OktaAuth,
restoreOriginalUri: RestoreOriginalUriFunction,
onAuthRequired?: OnAuthRequiredFunction,
onAuthExpired?: OnAuthExpiredFunction,
children?: React.ReactNode
} & React.HTMLAttributes<HTMLDivElement>> = ({
oktaAuth,
restoreOriginalUri,
onAuthRequired,
onAuthRequired,
onAuthExpired,
children
}) => {
const [authState, setAuthState] = React.useState(() => {
Expand Down Expand Up @@ -93,7 +95,8 @@ const Security: React.FC<{
<OktaContext.Provider value={{
oktaAuth,
authState,
_onAuthRequired: onAuthRequired
_onAuthRequired: onAuthRequired,
_onAuthExpired: onAuthExpired
}}>
{children}
</OktaContext.Provider>
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/harness/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const App: React.FC<{
history.push('/widget-login');
};

const onAuthExpired = () => {
console.error('onAuthExpired');
}

const restoreOriginalUri = async (_oktaAuth: OktaAuth, originalUri: string) => {
history.replace(toRelativeUrl(originalUri || '/', window.location.origin));
};
Expand All @@ -44,6 +48,7 @@ const App: React.FC<{
<Security
oktaAuth={oktaAuth}
onAuthRequired={customLogin ? onAuthRequired : undefined}
onAuthExpired={onAuthExpired}
restoreOriginalUri={restoreOriginalUri}
>
<Switch>
Expand Down
5 changes: 4 additions & 1 deletion test/e2e/harness/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ const oktaAuth = new OktaAuth({
issuer: ISSUER,
clientId: CLIENT_ID,
redirectUri,
pkce
pkce,
tokenManager: {
autoRenew: false
}
});

ReactDOM.render(
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",
"sourceMap": true
},
"include": [
"src"
Expand Down