Skip to content

Instantly share code, notes, and snippets.

@ngbrown
Last active June 18, 2021 09:24
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngbrown/0be839b1f56679e6d41bbe5f36538931 to your computer and use it in GitHub Desktop.
Save ngbrown/0be839b1f56679e6d41bbe5f36538931 to your computer and use it in GitHub Desktop.
useAuth React authentication with oidc-client
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