diff --git a/src/OktaContext.ts b/src/OktaContext.ts index bdab0a93..d303acab 100644 --- a/src/OktaContext.ts +++ b/src/OktaContext.ts @@ -14,6 +14,7 @@ import { AuthState, OktaAuth } from '@okta/okta-auth-js'; export type OnAuthRequiredFunction = (oktaAuth: OktaAuth) => Promise | void; export type OnAuthResumeFunction = () => void; +export type OnAuthExpiredFunction = (oktaAuth: OktaAuth) => Promise | void; export type RestoreOriginalUriFunction = (oktaAuth: OktaAuth, originalUri: string) => Promise | void; @@ -21,6 +22,7 @@ export interface IOktaContext { oktaAuth: OktaAuth; authState: AuthState | null; _onAuthRequired?: OnAuthRequiredFunction; + _onAuthExpired?: OnAuthExpiredFunction; } const OktaContext = React.createContext(null); diff --git a/src/SecureRoute.tsx b/src/SecureRoute.tsx index f3b9ea67..706c74ae 100644 --- a/src/SecureRoute.tsx +++ b/src/SecureRoute.tsx @@ -11,19 +11,22 @@ */ 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> = ({ - 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 () => { @@ -31,10 +34,25 @@ const SecureRoute: React.FC<{ 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; + 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); @@ -54,13 +72,12 @@ 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, @@ -68,10 +85,16 @@ const SecureRoute: React.FC<{ 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; } diff --git a/src/Security.tsx b/src/Security.tsx index 478da667..7360d98b 100644 --- a/src/Security.tsx +++ b/src/Security.tsx @@ -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> = ({ oktaAuth, restoreOriginalUri, - onAuthRequired, + onAuthRequired, + onAuthExpired, children }) => { const [authState, setAuthState] = React.useState(() => { @@ -93,7 +95,8 @@ const Security: React.FC<{ {children} diff --git a/test/e2e/harness/src/App.tsx b/test/e2e/harness/src/App.tsx index 54a92e4f..76966c85 100644 --- a/test/e2e/harness/src/App.tsx +++ b/test/e2e/harness/src/App.tsx @@ -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)); }; @@ -44,6 +48,7 @@ const App: React.FC<{ diff --git a/test/e2e/harness/src/index.tsx b/test/e2e/harness/src/index.tsx index f15e81e0..9320124e 100644 --- a/test/e2e/harness/src/index.tsx +++ b/test/e2e/harness/src/index.tsx @@ -32,7 +32,10 @@ const oktaAuth = new OktaAuth({ issuer: ISSUER, clientId: CLIENT_ID, redirectUri, - pkce + pkce, + tokenManager: { + autoRenew: false + } }); ReactDOM.render( diff --git a/test/e2e/harness/tsconfig.json b/test/e2e/harness/tsconfig.json index 1e57eea2..1a3f7ecd 100644 --- a/test/e2e/harness/tsconfig.json +++ b/test/e2e/harness/tsconfig.json @@ -17,7 +17,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "sourceMap": true }, "include": [ "src"