/** @module useAuth */
import '../models/auth';
import React, {
useState,
useRef,
useEffect,
useContext,
createContext,
useReducer,
} from 'react';
import { ACLS, AUTH_STATES } from '../constants';
import PropTypes from 'prop-types';
import { parseTokens, decodeTokenToJWT } from '../helpers/JWTUtil';
import axios from 'axios';
const openWindow = false;
let staleCheckTimeoutTracker = -1;
const timeToSessionIdle = 5 * 60; // The time in which a session is considered 'idle' and should not auto refresh the token
const timeToStale = 5 * 60; // The time from the token expiration when we should refresh the token if the user is not idle
/*
TODO: More documentation!
*/
// This constant is a template for a logged out user
// It gets used when a user is not logged in or when the logged in user selects to logout
// Note there are no permissions set for this user
// The default permissions come from the api as they will differ depending on the environment/tenant
const loggedOutUser = {
name: 'Anonymous',
isSignedIn: false,
};
/**
* Define the actions that can be dispatched
* These represent the different state changes that the auth state can be in
* @constant ACTIONS
*/
const ACTIONS = {
SET_CONFIG: 'SET_CONFIG',
BEGIN_LOGIN: 'begin_login',
SET_TOKEN_INFO: 'set_token_info',
BEGIN_LOGOUT: 'begin_logout',
FINISH_LOGOUT: 'finish_logout',
SET_ERROR: 'set_error',
SET_PERMISSIONS: 'set_permissions',
TOKEN_STALE: 'token_stale',
SET_STALE_CHECK_STATE: 'set_stale_check_state',
SET_LAST_REQUEST_TIME: 'set_last_request_time',
SET_REFRESHING: 'set_refreshing',
};
/**
* Define the states that the stale check can be in
* @constant STALE_CHECK_STATES
*/
const STALE_CHECK_STATES = {
NEW_SESSION: 'NEW_SESSION',
STALE_CHECK_REQUESTED: 'STALE_CHECK_REQUESTED',
STALE_CHECK_COMPLETE: 'STALE_CHECK_COMPLETE',
STALE_CHECK_IN_PROGRESS: 'STALE_CHECK_IN_PROGRESS',
};
/**
* Valid states that allow logins to occur
* If the auth state is not in this list, we will not allow logins
* This will, for example, not allow someone to attempt to log in while already logged in
* @constant VALID_LOGIN_STATES
*/
const VALID_LOGIN_STATES = [AUTH_STATES.INITIALIZING, AUTH_STATES.LOGGED_OUT, AUTH_STATES.TOKEN_STALE];
/**
* Define the initial state of the auth state
* @constant initialState
*/
const initialState = {
state: AUTH_STATES.INITIALIZING,
refreshToken: null,
bearerToken: null,
user: {
authenticated: false,
name: '',
id: '',
raw: {},
},
};
/**
* This is the context that will be used to provide the auth state to its consumers
* @constant authContext
*/
export const authContext = createContext();
let _metaWhileList = [];
let _staleCheckSeconds = 60;
// If multiple apps are using the same cookie, we need to be able to differentiate which one logged out
let APP_ID = '123-456';
/**
* Wrap the hook with a provider
* Use cookieReference to differentiate between multiple apps using the same cookie *
* @param {object} props - The props for the component
* @param {object} props.config - The config object
* @param {string} [props.config.cookieReference] - The cookie reference
* @param {array} [props.whitelist] - The whitelist array
* @param {object} [props.options] - Additional options
* @param {number} [props.options.staleCheckSeconds] - The stale check seconds
* @param {object} [props.children] - The children
* @function ProvideAuth
* @returns {React.Context} The auth context provider
*/
export const ProvideAuth = ({ config, whitelist, options, children }) => {
if (config?.cookieReference) {
APP_ID = config.cookieReference;
}
const auth = useProvideAuth(config, whitelist, options);
// We are providing the object returned by useProvideAuth as the value of the authContext
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};
ProvideAuth.propTypes = {
config: PropTypes.object.isRequired,
whitelist: PropTypes.array,
options: PropTypes.object,
children: PropTypes.element,
};
// Hook for child components to get the auth object ...
// ... and re-render when it changes.
/**
* This hook provides the auth state and methods to its consumers
* It is used to manage the auth state and provide the auth state to its consumers
* @function useAuth
* @returns {object} The auth object
*/
export const useAuth = () => {
// The context object will contain anything returned by useProvideAuth
const context = useContext(authContext);
if (context === undefined) {
throw new Error('useAuth must be used within a ProvideAuth');
}
return context;
};
/**
* These are the properties and methods that the "useAuth" hook provides to its consumers
* We are providing the auth state, login, and logout methods
* @param {object} config The configuration for the component
* @param {String[]} [whitelist] The whitelist array
* @param {object} [options] Additional options
* @returns {AuthContext} The auth context object
*/
const useProvideAuth = (config, whitelist, options) => {
// We are using the useReducer hook to manage the auth state
// authState should be exposed to the consumer as part of this hook
const [authState, dispatch] = useReducer(authReducer, initialState);
const intercept = useRef();
const [initInfo, setInitInfo] = useState(null);
useEffect(() => {
if (!initInfo) {
return;
}
if (whitelist?.length > 0) {
_metaWhileList = whitelist;
}
if (options?.staleCheckSeconds) {
_staleCheckSeconds = options?.staleCheckSeconds;
}
// Check to see if the config is valid
if (isValidConfig(config)) {
// Push the config into the state, we probably dont need the full config but it contains the anonymous user info at a minimum
dispatch({ type: ACTIONS.SET_CONFIG, config });
// If we have a boot token we will use it to login
if (initInfo.bootToken) {
dispatch({ type: ACTIONS.BEGIN_LOGIN });
startWithBootToken(initInfo.bootToken, initInfo.bootUser);
}
// If we have a refresh token we will use it to get a new id and access token
else if (initInfo.refreshToken) {
dispatch({ type: ACTIONS.BEGIN_LOGIN });
startWithRefreshToken(initInfo.refreshToken);
}
// The user is not logged in
else {
if (config.autoLogin) {
login();
}
else {
dispatch({ type: ACTIONS.FINISH_LOGOUT });
}
}
} else {
// If the config is not valid, we will set the state to be error
console.warn('useProvideAuth: config is invalid');
//dispatch({ type: ACTIONS.SET_ERROR, errorMessage: 'Invalid config' });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initInfo, config]);
// This will be run once and only once when is first called
useEffect(() => {
intercept.current = axios.interceptors.request.use((request) => {
request.headers['Content-Type'] = 'application/json';
const token = getActiveBearerToken();
if (token) {
request.headers.Authorization = `Bearer ${token}`;
}
dispatch({ type: ACTIONS.SET_LAST_REQUEST_TIME, time: Date.now() });
return request;
});
// Do not allow this to be called if the protocol is not https
// This is to prevent the user from being able to log in with http
// Http is insecure and should not be used
if (window.location.protocol !== 'https:') {
console.error('Only fools would try to use http');
throw new Error('Only fools would try to use http');
}
// Add our listener to the window
// If we login with a new tab this will receive the event
window.addEventListener('message', handleMessage);
async function boop() {
// get out initialization info from the session storage
const nextInit = {
refreshToken: await getRefreshTokenFromSession(),
subject: await getSubjectFromCookie(),
bootToken: await getBootTokenFromSession(),
bootUser: await getBootUserFromSession(),
};
setInitInfo(nextInit);
}
boop();
return () => {
if (intercept.current) {
axios.interceptors.request.eject(intercept.current);
}
window.removeEventListener('message', handleMessage);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
checkIfStale();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authState.staleCheckState]);
/**
* This function is called to start the login process
* It will open the login endpoint in a new tab
* Should be exposed to the consumer as part of this hook
* @param {objec | string} state The state to login with, this will be returned with a valid login
* @function
* @async
* @returns {boolean|Promise<void>} True if the login was started, false if the login was not started
*/
const login = async (state) => {
// Check to see if the auth state is valid for a login
// If not we will return false
if (!VALID_LOGIN_STATES.includes(authState.state)) {
console.error(authState, 'is not a valid login state');
return false;
}
// Dispatch the begin login action
dispatch({ type: ACTIONS.BEGIN_LOGIN });
// Check if we have a refresh token
// If we do, we will use it to get a new access token
// Otherwise check if we have a boot token
if (authState.refreshToken) {
return startWithRefreshToken(authState.refreshToken);
} else if (authState.bootToken) {
return startWithBootToken(authState.bootToken);
}
// If we don't have a refresh token or boot token, we will open a new tab to the login endpoint
// Calculate our redirect url based off of the origin of the current page and the login endpoint in our api
// const apiSlug = config.apiSlug || 'api';
let redirect = config.redirectUri || `${window.location.origin}/api/oauth/callback`;
let fetchUrl = `https://${config.host}/oauth2/authorize?response_type=code&client_id=${config.clientId}&redirect_uri=${redirect}`;
if (state) {
if (typeof state !== 'string') {
state = JSON.stringify(state);
}
fetchUrl += `&state=${state}`;
}
// If openWindow is true open the login endpoint in a new tab
// Otherwise open the login endpoint in the current tab
if (openWindow) window.open(fetchUrl, '_blank');
else window.location.href = fetchUrl;
};
/**
* This function is called to start the logout process
* It will open the logout endpoint in a new tab
* Should be exposed to the consumer as part of this hook
* @function
* @async
*/
const logout = async () => {
dispatch({ type: ACTIONS.BEGIN_LOGOUT });
logout_internal('logout');
// Calculate our redirect url based off of the origin of the current page and the logout endpoint in our api
const apiSlug = config.apiSlug || 'api';
const redirect = `${window.location.origin}/${apiSlug}/oauth/logout`;
// Open the logout endpoint in a new tab
const fetchUrl = config?.logoutURL || (config?.useAzureAD ?
`https://${config.host}/oauth2/logout?post_logout_redirect_uri=${redirect}` :
`https://${config.host}/logout?client_id=${config.clientId}&logout_uri=${redirect}`);
// If openWindow is true open the logout endpoint in a new tab
// Otherwise open the logout endpoint in the current tab
if (openWindow) window.open(fetchUrl, '_blank');
else window.location.href = fetchUrl;
};
/**
* Refresh the token
* @function
* @async
*/
const refresh = async () => {
const refreshToken = authState.refreshToken || await getRefreshTokenFromSession();
if (refreshToken) {
dispatch({ type: ACTIONS.SET_REFRESHING });
return startWithRefreshToken(refreshToken);
} else {
logout_internal('refresh else');
}
};
/**
* A Token has an expiration time
* When we get close to the expiration time we will check if the user is active
* If they are not active we will log them out
* If they are active we will refresh the token
* @function
* @async
*/
const checkIfStale = async () => {
if (authState.staleCheckState === STALE_CHECK_STATES.STALE_CHECK_REQUESTED) {
dispatch({ type: ACTIONS.SET_STALE_CHECK_STATE, staleCheckState: STALE_CHECK_STATES.STALE_CHECK_IN_PROGRESS });
const timeToExpired = getTimeToExpired();
// If the token expires in the next 5 minutes we will check if the user is active
let isStale = timeToExpired < timeToStale;
let userActive = authState.lastRequestTime && ((Date.now() - authState.lastRequestTime) / 1000 < timeToSessionIdle);
if (isStale) {
if (!userActive) {
logout_internal_soft();
} else {
const refreshToken = authState.refreshToken || await getRefreshTokenFromSession();
if (refreshToken) {
return startWithRefreshToken(refreshToken);
} else if (authState.bootToken) {
return startWithBootToken(authState.bootToken);
} else {
logout_internal_soft();
}
}
} else {
scheduleStaleCheck(_staleCheckSeconds);
return;
}
}
};
/**
* Method to decode a JWT token and determine the expiration time
* @function
* @returns {integer} the time in seconds until the token expires
*/
const getTimeToExpired = () => {
let decodedToken = decodeTokenToJWT(authState.bearerToken);
let expiresAt = decodedToken.exp;
let currentTime = Math.floor(Date.now() / 1000);
return expiresAt - currentTime;
};
/**
* This function will schedule a stale check in the future
* @param {integer} timeInSeconds the time in seconds to wait before triggering another stale check
* @function
*/
const scheduleStaleCheck = (timeInSeconds) => {
if (staleCheckTimeoutTracker !== -1) {
clearTimeout(staleCheckTimeoutTracker);
staleCheckTimeoutTracker = -1;
}
staleCheckTimeoutTracker = setTimeout(() => {
dispatch({ type: ACTIONS.SET_STALE_CHECK_STATE, staleCheckState: STALE_CHECK_STATES.STALE_CHECK_REQUESTED });
}, timeInSeconds * 1000);
};
/**
* Parse the tokens from the url and dispatch the appropriate actions
* @param {Array} tokenData the token data from the url
* @param {object} maybeUser the user object from the url
* @function
* @returns {object}
*/
const parseTokenAndUpdateState = async (tokenData, maybeUser) => {
// Check to see if we have a token
// If it is null or empty we will logout
if (!tokenData || tokenData.length === 0) {
logout_internal('parseTokenAndUpdateState no tokenData');
return;
}
// Pull the info out of the token
const { token, isExpired, user, refresh_token } = parseTokens(tokenData);
const subject = await getSubjectFromCookie();
//If we have a prior subject and it is different from the current subject we will logout
// We need to check against the APP_ID to allow an application that logged itself out to log back in
// If another application using this 'sub' cookie on the same domain logged out we will insure this session logs out
if (subject && subject !== APP_ID && subject !== user.sub) {
logout_internal('parseTokenAndUpdateState subject mismatch');
if (config.autoLogin) {
login();
}
return;
}
const bearerToken = token.access_token;
window.isExpired = isExpired;
scheduleStaleCheck(_staleCheckSeconds);
//TODO: Check if a token is expired
// If we have a refresh tokem we will set it in the session so it can be used when the page is refreshed
if (refresh_token) {
setRefreshTokenInSession(refresh_token);
}
// If the result doesnt contain the access control list set it to an empty list
if (!user.acl) user.acl = [];
// This may be a hack but if we have logged in we need to be sure the use can see the user menu
user.acl.push('Can Sign In');
// Set the isSignedIn on the user object to true
// TODO: This may be redundant to isAuthenticated
user.isSignedIn = true;
// Dispatch the finish login action
const meta = {};
if (_metaWhileList?.length && maybeUser) {
_metaWhileList.forEach((key) => {
if (maybeUser[key]) {
meta[key] = maybeUser[key];
}
});
}
dispatch({
type: ACTIONS.SET_TOKEN_INFO,
user,
meta,
bearerToken,
refreshToken: refresh_token || await getRefreshTokenFromSession(),
permissions: maybeUser?.permissions,
});
dispatch({
type: ACTIONS.SET_STALE_CHECK_STATE,
staleCheckState: STALE_CHECK_STATES.NEW_SESSION,
});
// Now we need to get the permissions for the user
if (!maybeUser || !maybeUser?.permissions) {
getPermissions(bearerToken); // The parameter is a hack because our state isnt updated yet. Tomorrow me problem.
}
/**
* This is a bear to remind us to set the bearer token in subsiquent api calls that need to be authenticated
https://en.wikipedia.org/wiki/Joan_Stark
_,-""`""-~`)
(`~_,=========\
|---,___.-.__,\
| o \ ___ _,,,,_ _.--.
\ `^` /`_.-"~ `~-;` \
\_ _ .' `, |
|`- \'__/
/ ,_ \ `'-.
/ .-""~~--. `"-, ;_ /
| \ \ | `""`
\__.--'`"-. /_ |'
`"` `~~~---.., |
jgs \ _.-'`-.
\ \
'. /
`"~"`
*/
};
/**
* Called to reset the state of the auth object
* @function
* @async
*/
const logout_internal = async () => {
// Dispatch the begin logout action
dispatch({ type: ACTIONS.BEGIN_LOGOUT });
// Reset our session
clearRefreshTokenInSession();
// Clear the cookie
await clearSubjectCookie();
// Dispatch the finish logout action
dispatch({ type: ACTIONS.FINISH_LOGOUT });
};
/**
* Dispatch the token stale action
* @function
* @async
*/
const logout_internal_soft = async () => {
// Dispatch the begin logout action
dispatch({ type: ACTIONS.TOKEN_STALE });
};
/**
* Attempt to refresh the token
* @async
* @function
* @param {*} refreshToken the token used to call the refresh endpoint
*/
const startWithRefreshToken = async (refreshToken) => {
const apiSlug = config.apiSlug || 'api';
const url = `${window.location.origin}/${apiSlug}/oauth/refresh`;
try {
axios.post(url, refreshToken, { headers: { 'content-type': 'application/x-www-form-urlencoded' } }).then(res => {
parseTokenAndUpdateState(res.data.token, res.data.user);
}
).catch(error => {
if (error.name !== 'CanceledError') {
console.error('Error fetching data', error);
}
logout_internal('startwithrefreshtoken axios catch');
}).finally(() => {
// todo: do we need to do anything here?
});
} catch (ex) {
console.error(`Failed to refresh token: ${ex}`);
logout_internal('startwithrefreshtoken failed refresh catch');
}
};
/**
* This is called if we discovered a boot token in the session
* This can happen when we authenticate without opening a new tab
* The oAuth helper will set the boot token in the session and then return to the current page
* @async
* @function
* @param {String} bootToken the token used to start the oAuth flow
* @param {Object|null} maybeBootUser the user object stored in the session to start the oAuth flow
*/
const startWithBootToken = async (bootToken, maybeBootUser) => {
// The boot token is our oAuth token
// It should be parsed as such
// Dispatch a begin login action
dispatch({ type: ACTIONS.BEGIN_LOGIN });
parseTokenAndUpdateState(bootToken, maybeBootUser);
};
/**
* This function handles messages posted to the window
* We expect the message to come from the same origin
* We then make sure that the event type is 'oauth-token'
* @param {*} event the event that was posted to the window from the oAuth tab
* @function
* deprecated
*/
const handleMessage = (event) => {
if (event.origin !== window.location.origin) {
return;
}
if (event.data.type === 'oauth-token') {
// Parse the tokens from the message and store them in the state
parseTokenAndUpdateState(event.data.token);
}
};
/**
* This function will call to the permissions endpoint to get the permissions for the user
* @param {String} bearerToken the bearer token to use for the call
* @async
* @function
*/
const getPermissions = async (bearerToken) => {
// We will; call to the /api/user/echo with our bearer token in order to get a response of the current ACLs
let url = `/api/user/permissions`;
bearerToken = bearerToken || authState.bearerToken;
let response = await fetch(url, {
method: 'GET',
redirect: 'manual',
cors: 'no-cors',
headers: {
Authorization: `Bearer ${bearerToken}`,
},
});
if (response.status === 200) {
let permissions = await response.json();
permissions.push(ACLS.SIGN_IN); // This may be a hack
dispatch({ type: ACTIONS.SET_PERMISSIONS, acl: permissions });
} else {
console.error(`Failed to get permissions: ${response.status}`);
}
};
/**
* Just return the bearer token
* @function
* @returns {String} the bearer token
*/
const getBearerToken = () => {
return authState.bearerToken;
};
// If we are in the development environment expose the getBearerToken method
// This is used to get the bearer token for the API calls
if (process.env.NODE_ENV === 'development') {
window.getBearerToken = getBearerToken;
}
return {
login,
logout,
refresh,
getPermissions,
getBearerToken,
authState,
};
};
let activeBearerToken = null;
/**
* Get the active bearer token
* @function
* @returns {String} the active bearer token
*/
const getActiveBearerToken = () => {
return activeBearerToken;
};
/**
* set the active bearer token
* @function
* @param {string} bearerToken
*/
const setActiveBearerToken = (bearerToken) => {
activeBearerToken = bearerToken;
};
/**
* Reducer hook method for the auth state
* @function
* @param {*} nextState
* @param {*} action
* @returns {object}
*/
const authReducer = (nextState, action) => {
switch (action.type) {
// Maintain the rest of the current state, only update the refresh token
case ACTIONS.SET_CONFIG: {
let anonUser = action.config?.anonUser;
if (anonUser) {
// Migrate from the backend model to the frontend model
anonUser = {
id: anonUser.id,
name: anonUser.name,
email: anonUser.email,
isSignedIn: false,
isAuthenticated: false,
acl: anonUser.acl || anonUser.permissions,
};
}
return {
...nextState,
loggedOutuser: anonUser,
config: action.config,
state: AUTH_STATES.LOGGED_OUT,
user: anonUser || loggedOutUser,
};
}
case ACTIONS.BEGIN_LOGIN:
return {
...nextState,
user: null,
state: AUTH_STATES.LOGGING_IN,
};
case ACTIONS.SET_TOKEN_INFO:
// We've moved setting the "LOGGED_IN" state to the ACTIONS.SET_PERMISSIONS login action
if (action.permissions) action.user.acl = action.permissions;
if (action.meta) {
action.user.meta = action.meta;
}
//TODO: This is a weird side effect
setActiveBearerToken(action.bearerToken);
if (action.refreshToken) {
setRefreshTokenInSession(action.refreshToken);
}
// If we have the sub claim, we can use that to set the subject in to cookie
if (action.user.sub) {
setSubjectInCookie(action.user.sub);
}
return {
...nextState,
user: action.user,
bearerToken: action.bearerToken,
refreshToken: action.refreshToken,
state: action.permissions ? AUTH_STATES.LOGGED_IN : nextState.state,
};
case ACTIONS.TOKEN_STALE: {
// We need to put a flag in the session to indicate that the token is stale and needs to be refreshed when the user performs an action
return {
...nextState,
state: AUTH_STATES.TOKEN_STALE,
};
}
case ACTIONS.SET_REFRESHING: {
return {
...nextState,
state: AUTH_STATES.REFRESHING_TOKEN,
};
}
case ACTIONS.SET_STALE_CHECK_STATE: {
return {
...nextState,
staleCheckState: action.staleCheckState,
};
}
case ACTIONS.SET_LAST_REQUEST_TIME: {
return {
...nextState,
lastRequestTime: action.time,
};
}
case ACTIONS.BEGIN_LOGOUT:
return {
...nextState,
state: AUTH_STATES.LOGGING_OUT,
};
case ACTIONS.FINISH_LOGOUT:
return {
...nextState,
user: nextState.loggedOutuser || {
...loggedOutUser,
acl: nextState.defaultACL,
},
bearerToken: null,
refreshToken: null,
state: AUTH_STATES.LOGGED_OUT,
};
case ACTIONS.SET_PERMISSIONS:
return {
...nextState,
state: AUTH_STATES.LOGGED_IN,
user: nextState.user?.isSignedIn
? { ...nextState.user, acl: action.acl }
: nextState.user,
};
case ACTIONS.SET_ERROR:
return {
error: action.error,
state: AUTH_STATES.ERROR,
};
default:
console.error(`Unknown action type: ${action.type}`);
return nextState;
}
};
/**
* This function will attempt to get the refresh token from session storage
* When we authenticate, if we are given a refresh token, we will store it in session storage
* This way we can use it to get a new access token when the page is reloaded or the user navigates to a new page
* @function
* @async
* @returns {String} the refresh token
*/
const getRefreshTokenFromSession = async () => {
try {
let session = window.sessionStorage.getItem('refreshToken') || window.localStorage.getItem('refreshToken');
if (session) {
return session;
}
} catch (ex) { }
return null;
};
/**
* This will attempt to get the subject from the cookie
* @function
* @async
* @returns {String} the subject
*/
const getSubjectFromCookie = async () => {
// Check to see if there is a cookie containing the sub
let subject = null;
try {
// FireFox doesn't support cookieStore
if (window.cookieStore) {
const cookie = await window.cookieStore.get('sub');
if (cookie) {
subject = cookie.value;
}
} else {
subject = document.cookie.split(';').find((c) => c.trim().startsWith('sub=')).split('=')[1];
}
}
catch (ex) {
console.debug('Error getting sub from cookie', ex);
}
return subject;
};
/**
* This function will attempt to get the boot token from session storage
* When we authenticate, if we are given a refresh token, we will store it in session storage
* This way we can use it to get a new access token when the page is reloaded or the user navigates to a new page
* @function
* @async
* @returns {String} the boot token
*/
const getBootTokenFromSession = async () => {
try {
let session = window.sessionStorage.getItem('bootToken');
if (session) {
window.sessionStorage.removeItem('bootToken');
return session;
}
} catch (ex) { }
return null;
};
/**
* This function will attempt to get the bootUser from session storage
* When we authenticate, if we are given a refresh token, we will store it in session storage
* This way we can use it to get a new access token when the page is reloaded or the user navigates to a new page
* @function
* @async
*/
const getBootUserFromSession = async () => {
try {
let session = window.sessionStorage.getItem('bootUser');
try {
session = JSON.parse(session);
} catch (ex) { }
if (session) {
window.sessionStorage.removeItem('bootUser');
return session;
}
} catch (ex) { }
return null;
};
/**
* Checks to see if the config object is valid
* @param {object} config
* @function
* @returns {boolean} true if the all the required bits of config are present
*/
const isValidConfig = (config) => {
let keys = Object.keys(config);
let hasClientId = keys.includes('clientId');
let hasRedirectUri = keys.includes('redirectUri');
let hasScopes = keys.includes('scopes');
let hasHost = keys.includes('host');
// TODO: Other checks?
return hasClientId && hasRedirectUri && hasScopes && hasHost;
};
/**
* This function will store the refresh token in session storage
* Later on we can retrieve it and use it to get a new access token
* @function
* @param {String} refreshToken
*/
const setRefreshTokenInSession = (refreshToken) => {
window.localStorage.setItem('refreshToken', refreshToken);
};
/**
* This function will set the subject in the cookie
* @function
* @param {String} subject
*/
const setSubjectInCookie = (subject) => {
// Set our subject cookie
try {
// FireFox doesn't support cookieStore
if (window.cookieStore) {
let domain = window.location.hostname;
domain = domain.split('.').slice(1).join('.');
let cookie = { name: 'sub', value: subject, sameSite: 'lax' };
if (domain && domain !== 'localhost') {
cookie.domain = domain;
}
window.cookieStore.set(cookie);
} else {
// Set or replace the cookie so that sub is set to the subject
const oldValue = document.cookie.split(';');
const newValue = oldValue.filter((c) => !c.trim().startsWith('sub='));
newValue.push(`sub=${subject}`);
document.cookie = newValue.join(';');
}
} catch (ex) {
console.debug('Error setting refreshToken cookie', ex);
}
};
/**
* This function will clear the refresh token from session storage
* @function
*/
const clearRefreshTokenInSession = () => {
window.sessionStorage.removeItem('refreshToken');
window.localStorage.removeItem('refreshToken');
};
/**
* This will clear the subject cookie
* @function
* @async
*/
const clearSubjectCookie = async () => {
let domain = window.location.hostname.split('.').slice(1).join('.');
try {
// FireFox doesn't support cookieStore
if (window.cookieStore) {
await window.cookieStore.delete('sub', { domain });
} else {
// Set or replace the cookie so that sub is set to the subject
const oldValue = document.cookie.split(';');
const newValue = oldValue.filter((c) => !c.trim().startsWith('sub='));
document.cookie = newValue.join(';');
}
} catch (ex) {
console.debug('Error clearing refreshToken cookie', ex);
}
// Sometimes the cookie wont delete, who knows why
// So we will set it to an empty string
// Setting to special string so the application can check it if logged itself out of it if another app did it
setSubjectInCookie(APP_ID);
};
Source