Last active
June 18, 2021 09:24
-
-
Save ngbrown/0be839b1f56679e6d41bbe5f36538931 to your computer and use it in GitHub Desktop.
useAuth React authentication with oidc-client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
createContext, | |
useReducer, | |
useEffect, | |
useState, | |
useContext, | |
} from 'react'; | |
import * as Oidc from 'oidc-client'; | |
// Inspired by https://github.com/Swizec/useAuth | |
type AuthState = { | |
user: Oidc.User | null; | |
expiresAt: number | null; | |
isAuthenticating: boolean; | |
isAuthenticated: boolean; | |
}; | |
const DEFAULT_STATE = { | |
user: null, | |
// milliseconds from Unix Epoc, 1970 UTC (pass to Date constructor) | |
expiresAt: null, | |
isAuthenticating: false, | |
isAuthenticated: false, | |
} as AuthState; | |
const AUTHENTICATION_STARTING = 'AUTHENTICATION_STARTING'; | |
const AUTHENTICATION_STOPPED = 'AUTHENTICATION_STOPPED'; | |
const LOGOUT_STARTING = 'LOGOUT_STARTING'; | |
const LOGOUT_STOPPED = 'LOGOUT_STOPPED'; | |
const USER_LOADED = 'USER_LOADED'; | |
const USER_UNLOADED = 'USER_UNLOADED'; | |
function authenticationStarting() { | |
return {type: AUTHENTICATION_STARTING}; | |
} | |
function authenticationStopped() { | |
return {type: AUTHENTICATION_STOPPED}; | |
} | |
function userLoaded(user: Oidc.User) { | |
return {type: USER_LOADED, payload: user}; | |
} | |
function userUnloaded() { | |
return {type: USER_UNLOADED}; | |
} | |
function logoutStarting() { | |
return {type: LOGOUT_STARTING}; | |
} | |
function logoutStopped() { | |
return {type: LOGOUT_STOPPED}; | |
} | |
type AuthAction = {type: string; payload?: any}; | |
function authReducer(state: AuthState, action: AuthAction): AuthState { | |
switch (action.type) { | |
case AUTHENTICATION_STARTING: | |
case LOGOUT_STARTING: | |
return {...state, isAuthenticating: true}; | |
case AUTHENTICATION_STOPPED: | |
case LOGOUT_STOPPED: | |
return {...state, isAuthenticating: false}; | |
case USER_LOADED: | |
return { | |
...state, | |
user: action.payload, | |
expiresAt: | |
action.payload.expires_at && action.payload.expires_at * 1000, | |
isAuthenticated: true, | |
}; | |
case USER_UNLOADED: | |
return { | |
...state, | |
isAuthenticated: false, | |
}; | |
default: | |
throw new Error(`Unknown action type '${action.type}'.`); | |
} | |
} | |
export const AuthContext = createContext({ | |
// when no matching provider | |
state: DEFAULT_STATE, | |
dispatch: (() => {}) as (action: AuthAction) => void, | |
oidcManager: null as Oidc.UserManager | null, | |
}); | |
function buildSigninRedirectState() { | |
return { | |
state: {location: window.location.href}, | |
}; | |
} | |
export function AuthProvider({ | |
children, | |
oidcSettings, | |
}: { | |
children: React.ReactChild; | |
oidcSettings: Oidc.UserManagerSettings; | |
}) { | |
const [state, dispatch] = useReducer(authReducer, DEFAULT_STATE); | |
// create oidc user manager once. Updating oidcSettings isn't supported. | |
const [{oidcManager}] = useState(() => { | |
Oidc.Log.logger = console; | |
const oidcManager = new Oidc.UserManager(oidcSettings); | |
oidcManager.events.addAccessTokenExpired(() => { | |
dispatch(userUnloaded()); | |
}); | |
oidcManager.events.addUserUnloaded(() => { | |
dispatch(userUnloaded()); | |
}); | |
oidcManager.events.addUserSignedOut(() => { | |
dispatch(userUnloaded()); | |
}); | |
oidcManager.events.addUserLoaded((user) => { | |
dispatch(userLoaded(user)); | |
}); | |
return {oidcManager}; | |
}); | |
const [contextValue, setContextValue] = useState({ | |
state, | |
dispatch, | |
oidcManager, | |
}); | |
useEffect(() => { | |
setContextValue((prev) => ({...prev, state})); | |
}, [state]); | |
// on mount | |
useEffect(() => { | |
async function tryAuthentication() { | |
dispatch(authenticationStarting()); | |
try { | |
const user = await oidcManager.getUser(); | |
if (user && !user.expired) { | |
console.info('User was already logged in.'); | |
dispatch(userLoaded(user)); | |
} else { | |
await login(); | |
} | |
} catch (err) { | |
console.error(`Caught error while loading user: ${err.message}`, err); | |
} | |
dispatch(authenticationStopped()); | |
} | |
async function login() { | |
console.info('User not logged in - attempting silent login...'); | |
try { | |
const user = await oidcManager.signinSilent(); | |
console.info('User silently logged in.'); | |
dispatch(userLoaded(user)); | |
} catch (err) { | |
console.info(`signinSilent failed - ${err.message}. Redirecting...`); | |
await oidcManager.signinRedirect(buildSigninRedirectState()); | |
} | |
} | |
tryAuthentication().catch((err) => | |
console.error('Error mounting AuthProvider', err) | |
); | |
}, [oidcManager]); | |
return ( | |
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider> | |
); | |
} | |
export function useAuth() { | |
const {state, dispatch, oidcManager} = useContext(AuthContext); | |
async function login() { | |
dispatch(authenticationStarting()); | |
if (oidcManager) { | |
try { | |
await oidcManager.signinRedirect(buildSigninRedirectState()); | |
} catch (err) { | |
console.error(`Caught error while signing in: ${err}`, err); | |
} | |
} else { | |
console.error('Surround use of useAuth with <AuthProvider/>'); | |
} | |
dispatch(authenticationStopped()); | |
} | |
async function logout() { | |
dispatch(logoutStarting()); | |
if (oidcManager) { | |
try { | |
await oidcManager.signoutRedirect(buildSigninRedirectState()); | |
} catch (err) { | |
console.error(`Caught error while signing out: ${err}`, err); | |
} | |
} else { | |
console.error('Surround use of useAuth with <AuthProvider/>'); | |
} | |
dispatch(logoutStopped()); | |
} | |
return { | |
...state, | |
userId: state.user?.profile?.name ?? null, | |
login, | |
logout, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment