Skip to content

Commit

Permalink
Merge pull request #1544 from tidepool-org/UPLOAD-936-native-sso
Browse files Browse the repository at this point in the history
[UPLOAD-936] [UPLOAD-1005] [UPLOAD-1023] native oauth login
  • Loading branch information
krystophv authored Apr 2, 2023
2 parents 6926bc1 + 6e61264 commit 4245c26
Show file tree
Hide file tree
Showing 18 changed files with 318 additions and 58 deletions.
22 changes: 20 additions & 2 deletions app/actions/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ let hostMap = {
'linux': 'linux',
};

const isBrowser = typeof window !== 'undefined';
let win = isBrowser ? window : null;

function createActionError(usrErrMessage, apiError) {
const err = new Error(usrErrMessage);
if (apiError) {
Expand Down Expand Up @@ -265,12 +268,27 @@ export function doLogout() {
api.user.logout((err) => {
if (err) {
dispatch(sync.logoutFailure());
dispatch(setPage(pages.LOGIN, actionSources.USER));
}
else {
dispatch(sync.logoutSuccess());
dispatch(setPage(pages.LOGIN, actionSources.USER));
}
dispatch(setPage(pages.LOGIN, actionSources.USER));
});
};
}

export function doLoggedOut() {
return (dispatch, getState) => {
const { api } = services;
dispatch(sync.logoutRequest());
api.user.logout((err) => {
if (err) {
dispatch(sync.logoutFailure());
}
else {
dispatch(sync.logoutSuccess());
}
dispatch(setPage(pages.LOGGED_OUT, actionSources.USER));
});
};
}
Expand Down
4 changes: 2 additions & 2 deletions app/actions/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -1045,10 +1045,10 @@ export function selectClinic(clinicId) {
};
}

export function keycloakReady(event, error){
export function keycloakReady(event, error, logoutUrl){
return {
type: ActionTypes.KEYCLOAK_READY,
payload: { error, event },
payload: { error, event, logoutUrl },
};
}

Expand Down
5 changes: 4 additions & 1 deletion app/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ export class Header extends Component {

render() {
const { allUsers, dropdown, location, keycloakConfig } = this.props;
if (location.pathname === pagesMap.LOADING) {
if (
location.pathname === pagesMap.LOADING ||
location.pathname === pagesMap.LOGGED_OUT
) {
return null;
}

Expand Down
54 changes: 54 additions & 0 deletions app/components/LoggedOut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import styles from '../../styles/components/LoggedOut.module.less';
import { useDispatch } from 'react-redux';

import actions from '../actions/';
const asyncActions = actions.async;
import { pages } from '../constants/otherConstants';
import * as actionSources from '../constants/actionSources';
import logo from '../../images/Tidepool_Logo_Light x2.png';

const remote = require('@electron/remote');
const i18n = remote.getGlobal('i18n');

export const LoggedOut = () => {
const dispatch = useDispatch();

const handleReturn = (e) => {
e.preventDefault();
dispatch(asyncActions.setPage(pages.LOGIN, actionSources.USER));
};

return (
<div className={styles.loggedOutPage}>
<div className={styles.logoWrapper}>
<img className={styles.logo} src={logo} />
</div>
<hr className={styles.hr} />
<div className={styles.heroText}>
{i18n.t('You have been signed out of your session.')}
</div>
<div className={styles.explainer}>
{i18n.t(
'For security reasons, we automatically sign you out after a certain period of inactivity, or if you\'ve signed out from another browser tab.'
)}
</div>
<div className={styles.explainer}>
{i18n.t('Please sign in again to continue.')}
</div>
<form className={styles.form}>
<div className={styles.actions}>
<button
type="submit"
className={styles.button}
onClick={handleReturn}
>
{i18n.t('Return to Login')}
</button>
</div>
</form>
</div>
);
};

export default LoggedOut;
2 changes: 1 addition & 1 deletion app/components/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const Login = () => {
const handleLogin = (e) => {
e.preventDefault();
if (keycloakConfig.initialized) {
keycloak.login();
window.open(keycloak.createLoginUrl(), '_blank');
} else {
dispatch(asyncActions.doLogin({ username, password }, { remember }));
}
Expand Down
2 changes: 2 additions & 0 deletions app/constants/otherConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const pages = {
CLINIC_USER_SELECT: 'CLINIC_USER_SELECT',
CLINIC_USER_EDIT: 'CLINIC_USER_EDIT',
WORKSPACE_SWITCH: 'WORKSPACE_SWITCH',
LOGGED_OUT: 'LOGGED_OUT',
};

export const pagesMap = {
Expand All @@ -35,6 +36,7 @@ export const pagesMap = {
CLINIC_USER_SELECT: '/clinic_user_select',
CLINIC_USER_EDIT: '/clinic_user_edit',
WORKSPACE_SWITCH: '/workspace_switch',
LOGGED_OUT: '/logged_out'
};

export const paths = {
Expand Down
2 changes: 2 additions & 0 deletions app/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import UpdateDriverModal from '../components/UpdateDriverModal';
import DeviceTimeModal from '../components/DeviceTimeModal';
import AdHocModal from '../components/AdHocModal';
import BluetoothModal from '../components/BluetoothModal';
import LoggedOut from '../components/LoggedOut.js';

import styles from '../../styles/components/App.module.less';

Expand Down Expand Up @@ -214,6 +215,7 @@ export class App extends Component {
<Route path="/clinic_user_edit" component={ClinicUserEditPage} />
<Route path="/no_upload_targets" component={NoUploadTargetsPage} />
<Route path="/workspace_switch" component={WorkspacePage} />
<Route path="/logged_out" component={LoggedOut} />
</Switch>
<Footer version={config.version} environment={this.state.server} />
{/* VersionCheck as overlay */}
Expand Down
59 changes: 44 additions & 15 deletions app/keycloak.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const updateKeycloakConfig = (info, store) => {
keycloak = new Keycloak({
url: info.url,
realm: info.realm,
clientId: 'tidepool-uploader',
clientId: 'tidepool-uploader-sso',
});
store.dispatch(sync.keycloakInstantiated());
} else {
Expand All @@ -32,7 +32,10 @@ const updateKeycloakConfig = (info, store) => {
const onKeycloakEvent = (store) => (event, error) => {
switch (event) {
case 'onReady': {
store.dispatch(sync.keycloakReady(event, error));
let logoutUrl = keycloak.createLogoutUrl({
redirectUri: 'tidepooluploader://localhost/keycloak-redirect'
});
store.dispatch(sync.keycloakReady(event, error, logoutUrl));
break;
}
case 'onInitError': {
Expand All @@ -57,6 +60,7 @@ const onKeycloakEvent = (store) => (event, error) => {
}
case 'onAuthRefreshError': {
store.dispatch(sync.keycloakAuthRefreshError(event, error));
store.dispatch(async.doLoggedOut());
break;
}
case 'onTokenExpired': {
Expand All @@ -65,6 +69,7 @@ const onKeycloakEvent = (store) => (event, error) => {
}
case 'onAuthLogout': {
store.dispatch(sync.keycloakAuthLogout(event, error));
store.dispatch(async.doLoggedOut());
break;
}
default:
Expand All @@ -86,10 +91,6 @@ const onKeycloakTokens = (store) => (tokens) => {

export const keycloakMiddleware = (api) => (storeAPI) => (next) => (action) => {
switch (action.type) {
case ActionTypes.LOGOUT_REQUEST: {
keycloak?.logout();
break;
}
case ActionTypes.FETCH_INFO_SUCCESS: {
if (!_.isEqual(_keycloakConfig, action.payload?.info?.auth)) {
updateKeycloakConfig(action.payload?.info?.auth, storeAPI);
Expand Down Expand Up @@ -121,37 +122,64 @@ export const keycloakMiddleware = (api) => (storeAPI) => (next) => (action) => {
}
break;
}
default:
case ActionTypes.LOGOUT_SUCCESS:
case ActionTypes.LOGOUT_FAILURE: {
keycloak.clearToken();
}
default:{
if (
action?.error?.status === 401 ||
action?.error?.originalError?.status === 401 ||
action?.error?.status === 403 ||
action?.error?.originalError?.status === 403
) {
// on any action with a 401 or 403, we try to refresh to keycloak token to verify
// if the user is still logged in
keycloak.updateToken(-1);
}
break;
}
}
return next(action);
};

let keyCount = 0;

export const KeycloakWrapper = (props) => {
const keycloakConfig = useSelector((state) => state.keycloakConfig);
const [, setHash] = useState(window.location.hash);
const blipUrl = useSelector((state) => state.blipUrls.blipUrl);
const blipRedirect = useMemo(() => {
if (!blipUrl) return null;
let url = new URL(`${blipUrl}upload-redirect`);
return url.href;
}, [blipUrl]);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const store = useStore();
const initOptions = useMemo(
() => ({
onLoad: 'check-sso',
checkLoginIframe: false,
enableLogging: process.env.NODE_ENV === 'development',
redirectUri: 'tidepooluploader://localhost/keycloak-redirect'
redirectUri: blipRedirect,
}),
[]
[blipRedirect]
);

const onEvent = useCallback(onKeycloakEvent(store), [store]);
const onTokens = useCallback(onKeycloakTokens(store), [store]);

// watch for hash changes and re-instantiate the authClient and force a re-render of the provider
// incrementing externally defined `key` forces unmount/remount as provider doesn't expect to
// have the authClient refreshed and only sets up refresh timeout on mount
const onHashChange = useCallback(() => {
keycloak = new Keycloak({
url: keycloakConfig.url,
realm: keycloakConfig.realm,
clientId: 'tidepool-uploader',
clientId: 'tidepool-uploader-sso',
});
setHash(window.location.hash);
}, [keycloakConfig.realm, keycloakConfig.url]);
keyCount++;
forceUpdate();
}, [keycloakConfig.realm, keycloakConfig.url, blipRedirect]);

useEffect(() => {
window.addEventListener('hashchange', onHashChange, false);
Expand All @@ -160,13 +188,14 @@ export const KeycloakWrapper = (props) => {
};
}, [onHashChange]);

if (keycloakConfig.url && keycloakConfig.instantiated) {
if (keycloakConfig.url && keycloakConfig.instantiated && blipRedirect) {
return (
<ReactKeycloakProvider
authClient={keycloak}
onEvent={onEvent}
onTokens={onTokens}
initOptions={initOptions}
key={keyCount}
>
{props.children}
</ReactKeycloakProvider>
Expand Down
Loading

0 comments on commit 4245c26

Please sign in to comment.