diff --git a/package.json b/package.json index 9e5c99dc7..1162f495e 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "src/js/base/**/*.js", "src/js/auth.js", "src/js/auth0.js", + "src/js/auth/*.js", "src/js/formapp.js", "src/js/formapp/**/*.js", "src/js/formservice.js", diff --git a/src/js/auth.js b/src/js/auth.js index 8a6c65c87..2fa221779 100644 --- a/src/js/auth.js +++ b/src/js/auth.js @@ -1,53 +1,35 @@ -import { extend } from 'underscore'; import Radio from 'backbone.radio'; -import createKindeClient from '@kinde-oss/kinde-auth-pkce-js'; -import { kindeConfig as config, appConfig } from './config'; - -import { auth as auth0, logout as auth0Logout, setToken as auth0SetToken, getToken as auth0GetToken } from './auth0'; +import * as auth0 from './auth/auth0'; +import * as none from './auth/none'; +import * as kinde from './auth/kinde'; import 'scss/app-root.scss'; -import { LoginPromptView } from 'js/views/globals/prelogin/prelogin_views'; +let authAgent; -const PATH_ROOT = '/'; -const PATH_RWELL = '/rw'; -const PATH_AUTHD = '/authenticated'; -const PATH_LOGIN = '/login'; -const PATH_LOGOUT = '/logout'; +function getAuthAgent() { + if (none.should()) return none; -function shouldAuth0() { - return !config.createParams; -} + if (authAgent) return authAgent; -let kinde; -let token; + // These should be ordered by priority lowest to highest + if (auth0.should()) authAgent = auth0; + if (kinde.should()) authAgent = kinde; -// Sets a token when not using auth0; -function setToken(tokenString) { - if (shouldAuth0()) return auth0SetToken(tokenString); + return authAgent; +} - token = tokenString; +function setToken(tokenString) { + getAuthAgent().setToken(tokenString); } function getToken() { - if (shouldAuth0()) return auth0GetToken(); - - if (token) return token; - if (!kinde || !navigator.onLine) return; - - return kinde - .getToken() - .catch(() => { - logout(); - }); + getAuthAgent().getToken(); } function logout() { - if (shouldAuth0()) return auth0Logout(); - - token = null; - window.location = '/logout'; + getAuthAgent().logout(); } Radio.reply('auth', { @@ -56,115 +38,8 @@ Radio.reply('auth', { getToken, }); -/* - * Modifies the current history state - */ -function replaceState(state) { - window.history.replaceState({}, document.title, state); -} - -function registerKinde(path, connection) { - kinde.register({ - app_state: { path }, - authUrlParams: { connection_id: connection }, - }); -} - -function login(path = PATH_ROOT, connection = config.connections.default) { - // iframe buster - if (top !== self) { - top.location = PATH_LOGIN; - return; - } - - replaceState(PATH_LOGIN); - - if (appConfig.disableLoginPrompt) { - registerKinde(path, connection); - return; - } - - const loginPromptView = new LoginPromptView(); - - loginPromptView.on('click:login', ()=> { - registerKinde(path, connection); - }); - - loginPromptView.render(); -} - -function shouldAuth() { - if (appConfig.cypress) { - setToken(appConfig.cypress); - return false; - } - - if (!navigator.onLine) { - return false; - } - - return true; -} - -async function createKinde(success) { - const kindeCreateParams = { - redirect_uri: location.origin + PATH_AUTHD, - logout_uri: location.origin, - on_redirect_callback: (user, { path } = {}) => { - if (!user) { - login(path); - return; - } - - if (path === PATH_LOGIN) path = PATH_ROOT; - - if (path === PATH_RWELL) { - path = PATH_ROOT; - localStorage.setItem(PATH_RWELL, 1); - } - - replaceState(path); - - success(); - }, - }; - - return createKindeClient(extend(kindeCreateParams, config.createParams)); -} - -/* - * Requests kinde authorization - * And authenticates authorization if kinde redirected to PATH_AUTHD - */ async function auth(success) { - if (!shouldAuth()) return success(); - - if (shouldAuth0()) return auth0(success); - - // NOTE: Set path before await create to avoid redirect replaceState changing the value - const pathName = location.pathname; - - kinde = await createKinde(success); - - if (pathName === PATH_AUTHD) return; - - if (pathName === PATH_LOGOUT) { - kinde.logout(); - return; - } - - // RWell specific login - if (pathName === PATH_RWELL || localStorage.getItem(PATH_RWELL)) { - login(PATH_RWELL, config.connections.roundingwell); - return; - } - - if (!await kinde.getUser()) { - login(pathName); - return; - } - - success(); + getAuthAgent().auth(success); } export { diff --git a/src/js/auth0.js b/src/js/auth/auth0.js similarity index 71% rename from src/js/auth0.js rename to src/js/auth/auth0.js index 20a127b4a..8c4ca7286 100644 --- a/src/js/auth0.js +++ b/src/js/auth/auth0.js @@ -1,27 +1,27 @@ -import { extend } from 'underscore'; +import { extend, isEmpty } from 'underscore'; import Radio from 'backbone.radio'; import { createAuth0Client } from '@auth0/auth0-spa-js'; -import { auth0Config as config, appConfig } from './config'; - -import 'scss/app-root.scss'; +import { auth0Config as config, appConfig } from 'js/config'; import { LoginPromptView } from 'js/views/globals/prelogin/prelogin_views'; -const RWELL_KEY = 'rw'; +import { PATH_ROOT, PATH_RWELL, PATH_AUTHD, PATH_LOGIN, PATH_LOGOUT } from './config'; + const RWELL_CONNECTION = 'google-oauth2'; -const AUTHD_PATH = '/authenticated'; let auth0; +let token; + +function should() { + return !isEmpty(config); +} function setAuth0(auth0Client) { auth0 = auth0Client; return auth0.isAuthenticated(); } -let token; - -// Sets a token when not using auth0; function setToken(tokenString) { token = tokenString; } @@ -53,18 +53,18 @@ function replaceState(state) { function authenticate(success) { return auth0.handleRedirectCallback() .then(({ appState }) => { - if (appState === '/login') appState = '/'; + if (appState === PATH_LOGIN) appState = PATH_ROOT; - if (appState === RWELL_KEY) { - appState = '/'; - localStorage.setItem(RWELL_KEY, 1); + if (appState === PATH_RWELL) { + appState = PATH_ROOT; + localStorage.setItem(PATH_RWELL, 1); } replaceState(appState); success(); }) .catch(() => { - forceLogin(); + login(); }); } @@ -73,47 +73,67 @@ function authenticate(success) { */ function getConfig() { config.authorizationParams = extend({ - redirect_uri: location.origin + AUTHD_PATH, + redirect_uri: location.origin + PATH_AUTHD, audience: 'care-ops-backend', }, config.authorizationParams); - if (localStorage.getItem(RWELL_KEY)) { + if (localStorage.getItem(PATH_RWELL)) { config.authorizationParams.connection = RWELL_CONNECTION; } return config; } +function logout() { + window.location = PATH_LOGOUT; +} + +function loginWithRedirect(opts) { + auth0.loginWithRedirect(extend({ prompt: 'login' }, opts)); +} + +function login(appState = PATH_ROOT) { + // iframe buster + if (top !== self) { + top.location = PATH_LOGIN; + return; + } + + if (appConfig.disableLoginPrompt) { + return loginWithRedirect({ appState }); + } + + replaceState(PATH_LOGIN); + + const loginPromptView = new LoginPromptView(); + + loginPromptView.on('click:login', ()=> { + loginWithRedirect({ appState }); + }); + + loginPromptView.render(); +} + /* * login will occur for any pre-auth flow * initially requesting auth0 authorization * And authenticating authorization if auth0 redirected to AUTHD_PATH */ function auth(success) { - if (appConfig.cypress) { - setToken(appConfig.cypress); - success(); - return; - } - - if (!navigator.onLine) { - success(); - return; - } - createAuth0Client(getConfig()) .then(setAuth0) .then(isAuthenticated => { - if (location.pathname === '/logout') { + if (location.pathname === PATH_LOGOUT) { + token = null; const federated = Radio.request('settings', 'get', 'federated_logout'); auth0.logout({ logoutParams: { returnTo: location.origin, federated } }); return; } // RWell specific login - if (location.pathname === `/${ RWELL_KEY }`) { + if (location.pathname === PATH_RWELL) { loginWithRedirect({ - appState: RWELL_KEY, + appState: PATH_RWELL, authorizationParams: { connection: RWELL_CONNECTION, }, @@ -121,58 +141,28 @@ function auth(success) { return; } - if (location.pathname === AUTHD_PATH) { + if (location.pathname === PATH_AUTHD) { authenticate(success); return; } if (!isAuthenticated) { - forceLogin(location.pathname); + login(location.pathname); return; } - if (location.pathname === '/login') { - replaceState('/'); + if (location.pathname === PATH_LOGIN) { + replaceState(PATH_ROOT); } success(); }); } -function logout() { - token = null; - window.location = '/logout'; -} - -function loginWithRedirect(opts) { - auth0.loginWithRedirect(extend({ prompt: 'login' }, opts)); -} - -function forceLogin(appState = '/') { - // iframe buster - if (top !== self) { - top.location = '/login'; - return; - } - - if (appConfig.disableLoginPrompt) { - return loginWithRedirect({ appState }); - } - - replaceState('/login'); - - const loginPromptView = new LoginPromptView(); - - loginPromptView.on('click:login', ()=> { - loginWithRedirect({ appState }); - }); - - loginPromptView.render(); -} - export { auth, logout, setToken, getToken, + should, }; diff --git a/src/js/auth/config.js b/src/js/auth/config.js new file mode 100644 index 000000000..9e0556f9c --- /dev/null +++ b/src/js/auth/config.js @@ -0,0 +1,5 @@ +export const PATH_ROOT = '/'; +export const PATH_RWELL = '/rw'; +export const PATH_AUTHD = '/authenticated'; +export const PATH_LOGIN = '/login'; +export const PATH_LOGOUT = '/logout'; diff --git a/src/js/auth/kinde.js b/src/js/auth/kinde.js new file mode 100644 index 000000000..c6573a510 --- /dev/null +++ b/src/js/auth/kinde.js @@ -0,0 +1,138 @@ +import { extend, isEmpty } from 'underscore'; +import createKindeClient from '@kinde-oss/kinde-auth-pkce-js'; + +import { kindeConfig as config, appConfig } from 'js/config'; + +import { LoginPromptView } from 'js/views/globals/prelogin/prelogin_views'; + +import { PATH_ROOT, PATH_RWELL, PATH_AUTHD, PATH_LOGIN, PATH_LOGOUT } from './config'; + +let kinde; +let token; + +function should() { + return !isEmpty(config); +} + +function setToken(tokenString) { + token = tokenString; +} + +function getToken() { + if (token) return token; + if (!kinde || !navigator.onLine) return; + + return kinde + .getToken() + .catch(() => { + logout(); + }); +} + +/* + * Modifies the current history state + */ +function replaceState(state) { + window.history.replaceState({}, document.title, state); +} + +function registerKinde(path, connection) { + kinde.register({ + app_state: { path }, + authUrlParams: { connection_id: connection }, + }); +} + + +async function createKinde(success) { + const kindeCreateParams = { + redirect_uri: location.origin + PATH_AUTHD, + logout_uri: location.origin, + on_redirect_callback: (user, { path } = {}) => { + if (!user) { + login(path); + return; + } + + if (path === PATH_LOGIN) path = PATH_ROOT; + + if (path === PATH_RWELL) { + path = PATH_ROOT; + localStorage.setItem(PATH_RWELL, 1); + } + + replaceState(path); + + success(); + }, + }; + + return createKindeClient(extend(kindeCreateParams, config.createParams)); +} + +function logout() { + window.location = PATH_LOGOUT; +} + +function login(path = PATH_ROOT, connection = config.connections.default) { + // iframe buster + if (top !== self) { + top.location = PATH_LOGIN; + return; + } + + replaceState(PATH_LOGIN); + + if (appConfig.disableLoginPrompt) { + registerKinde(path, connection); + return; + } + + const loginPromptView = new LoginPromptView(); + + loginPromptView.on('click:login', ()=> { + registerKinde(path, connection); + }); + + loginPromptView.render(); +} + +/* + * Requests kinde authorization + * And authenticates authorization if kinde redirected to PATH_AUTHD + */ +async function auth(success) { + // NOTE: Set path before await create to avoid redirect replaceState changing the value + const pathName = location.pathname; + + kinde = await createKinde(success); + + if (pathName === PATH_AUTHD) return; + + if (pathName === PATH_LOGOUT) { + token = null; + kinde.logout(); + return; + } + + // RWell specific login + if (pathName === PATH_RWELL || localStorage.getItem(PATH_RWELL)) { + login(PATH_RWELL, config.connections.roundingwell); + return; + } + + if (!await kinde.getUser()) { + login(pathName); + return; + } + + success(); +} + +export { + auth, + logout, + setToken, + getToken, + should, +}; diff --git a/src/js/auth/none.js b/src/js/auth/none.js new file mode 100644 index 000000000..be0d1c6dd --- /dev/null +++ b/src/js/auth/none.js @@ -0,0 +1,38 @@ +import { appConfig } from 'js/config'; +import { PATH_ROOT, PATH_LOGOUT } from './config'; + +let token; + +function should() { + return appConfig.cypress || !navigator.onLine; +} + +function setToken(tokenString) { + token = tokenString; +} + +function getToken() { + return token; +} + +function logout() { + window.location = PATH_LOGOUT; +} + +function auth(success) { + if (location.pathname === PATH_LOGOUT) { + token = null; + window.location = PATH_ROOT; + return; + } + + return success(); +} + +export { + auth, + logout, + setToken, + getToken, + should, +};