Skip to content

Commit

Permalink
Merge pull request #20 from uwcirg/feature/post-logout-uri-config
Browse files Browse the repository at this point in the history
Feature/post logout uri config
  • Loading branch information
daniellrgn committed Apr 23, 2024
2 parents 8ac5cb8 + d6a73b6 commit ed80f0b
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 21 deletions.
36 changes: 33 additions & 3 deletions default.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ COMPOSE_FILE=docker-compose.yaml:docker-compose.static-ingress.yaml
# Enable to use development overrides
# COMPOSE_FILE=docker-compose.yaml:docker-compose.dev.yaml

VITE_EPIC_CLIENT_ID=
VITE_CERNER_CLIENT_ID=

# SHL Server API endpoint url
VITE_API_BASE=

Expand All @@ -34,3 +31,36 @@ VITE_VIEWER_BASE=

# FHIR Server endpoint url
VITE_INTERMEDIATE_FHIR_SERVER_BASE=

# Value of identifier.system needed to query Patients based on KC id
VITE_FHIR_R4_EXTERNAL_ID_SYSTEM=https://keycloak.ltt.cirg.uw.edu

# OIDC client id
VITE_SOF_CLIENT_ID=shl_creator

# FHIR server endpoint for auth and queries
VITE_SOF_ISS=https://fhir-auth.inform.ubu.dlorigan.dev.cirg.uw.edu/fhir

# URL for back button
VITE_BACK_URL=https://inform.ubu.dlorigan.dev.cirg.uw.edu/pro_reports/clinic_report_inform

# OIDC server base url
VITE_OIDC_SERVER_BASE=https://keycloak.inform.ubu.dlorigan.dev.cirg.uw.edu

# Iframe url for session status updates
VITE_OIDC_CHECK_SESSION_IFRAME=https://keycloak.inform.ubu.dlorigan.dev.cirg.uw.edu/realms/ltt/protocol/openid-connect/login-status-iframe.html

# URL to redirect to for logout
VITE_OIDC_LOGOUT_ENDPOINT=https://keycloak.inform.ubu.dlorigan.dev.cirg.uw.edu/realms/ltt/protocol/openid-connect/logout

# URL to redirect to after logout completes
VITE_POST_LOGOUT_REDIRECT_URI=https://inform.ubu.dlorigan.dev.cirg.uw.edu/users

# Maximum allowable time without clicks, scrolling or tab switching; HH:MM:SS
# Defaults to 4 hours
#VITE_INACTIVITY_TIMEOUT=

# Maximum allowable time without clicks, scrolling or tab switching; HH:MM:SS
# Only applies when unable to perform standard session checks via iframe (e.g. Safari)
# Defaults to 15 minutes
#VITE_BACKUP_INACTIVITY_TIMEOUT=
2 changes: 2 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ interface ImportMetaEnv {
readonly VITE_BACK_URL: string
readonly VITE_OIDC_SERVER_BASE: string
readonly VITE_OIDC_LOGOUT_ENDPOINT: string
readonly VITE_POST_LOGOUT_REDIRECT_URI: string
readonly VITE_OIDC_CHECK_SESSION_IFRAME: string
readonly VITE_INACTIVITY_TIMEOUT: string
readonly VITE_BACKUP_INACTIVITY_TIMEOUT: string
readonly DEV_SERVER_PORT: number
}

Expand Down
72 changes: 58 additions & 14 deletions src/lib/SessionStatus.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
<script lang="ts">
import { onMount, getContext } from "svelte";
import { OIDC_BASE, CHECK_SESSION_IFRAME } from "./config";
import {
OIDC_BASE,
CHECK_SESSION_IFRAME,
BACKUP_INACTIVITY_TIMEOUT } from "./config";
import { goto } from "$app/navigation";
import type { SOFClient } from "./sofClient";
let sofClient: SOFClient = getContext('sofClient');
// Poll the OP iframe periodically
let checkSession = setInterval(checkSessionStatus, 5000);
let validatingSession = true;
let sessionCheckValid: boolean;
onMount(() => {
// Listen for messages from OP iframe
window.addEventListener('message', processStatus);
// Check session on load
// checkSessionStatus();
// Check session status on iframe load
let iframe = document.getElementById('opIframe');
iframe?.addEventListener('load', () => {
// Poll the iframe on load
checkSessionStatus();
});
// Check status as soon as the tab is refocused
Expand All @@ -29,26 +33,66 @@
function checkSessionStatus() {
var opIframe = document.getElementById('opIframe');
var message = `${sofClient.getClient().getState('clientId')} ${sofClient.getClient().getState('tokenResponse.session_state')}`;
var clientId = sofClient.getClient().getState('clientId');
var sessionId = sofClient.getClient().getState('tokenResponse.session_state');
var message = `${clientId} ${sessionId}`;
// Send message to OP iframe
opIframe?.contentWindow?.postMessage(message, OIDC_BASE);
}
let backupInactivityTimer: NodeJS.Timeout | undefined;
function resetBackupInactivityTimer() {
if (backupInactivityTimer !== undefined) {
clearTimeout(backupInactivityTimer);
}
backupInactivityTimer = undefined;
backupInactivityTimer = setTimeout(() => goto('/logout'), BACKUP_INACTIVITY_TIMEOUT);
}
function onVisible_resetBackupInactivityTimer() {
if (document.visibilityState === "visible") {
resetBackupInactivityTimer();
}
}
async function processStatus(event: any) {
if (event.origin === OIDC_BASE) {
var data = event.data;
if (data === 'changed') {
try {
let res = await sofClient.getClient().refresh();
if (!sofClient?.getClient()?.getState("tokenResponse.access_token")) {
throw Error("Unable to refresh token after session state change. Logging out.");
if (validatingSession || (sessionCheckValid !== undefined && !sessionCheckValid)) {
// Don't check status if it's only been erroring, as in Safari w/o 3p cookies
// If check has been 'error' but is now something else, change state to valid
sessionCheckValid = (data !== 'error');
if (validatingSession && !sessionCheckValid) {
// Check has only errored, initialize timeout (or manual logout)
resetBackupInactivityTimer();
document.addEventListener('click', resetBackupInactivityTimer);
document.addEventListener('scroll', resetBackupInactivityTimer);
document.addEventListener('visibilitychange', onVisible_resetBackupInactivityTimer);
}
validatingSession = false;
}
if (sessionCheckValid) {
// If check has been valid at some point, base action on result
// Clean up timeout and listeners if they were added before
checkSession = undefined;
document.removeEventListener('click', resetBackupInactivityTimer);
document.removeEventListener('scroll', resetBackupInactivityTimer);
document.removeEventListener('visibilitychange', onVisible_resetBackupInactivityTimer);
if (data === 'changed') {
try {
let res = await sofClient.getClient().refresh();
if (!sofClient?.getClient()?.getState("tokenResponse.access_token")) {
throw Error("Unable to refresh token after session state change. Logging out.");
}
} catch (e) {
console.error(e);
goto('/logout');
}
} catch (e) {
console.error(e);
} else if (data === 'error') {
goto('/logout');
}
} else if (data === 'error') {
goto('/logout');
} else {
console.warn('Unable to verify session state. You will be logged out automatically after 15 minutes of inactivity.');
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ export const INTERMEDIATE_FHIR_SERVER_BASE = import.meta.env.VITE_INTERMEDIATE_F
export const OIDC_BASE = import.meta.env.VITE_OIDC_SERVER_BASE;
export const CHECK_SESSION_IFRAME = import.meta.env.VITE_OIDC_CHECK_SESSION_IFRAME;
export const LOGOUT_URL = import.meta.env.VITE_OIDC_LOGOUT_ENDPOINT;
export const POST_LOGOUT_REDIRECT_URI = import.meta.env.VITE_POST_LOGOUT_REDIRECT_URI;

export const BACK_URL = import.meta.env.VITE_BACK_URL;

const timeout = (import.meta.env.VITE_INACTIVITY_TIMEOUT ?? "04:00:00").split(":").map((n) => Number(n));
const default_timout = "04:00:00";
const timeout = (import.meta.env.VITE_INACTIVITY_TIMEOUT ?? default_timout).split(":").map((n) => Number(n));
export const INACTIVITY_TIMEOUT = toMilliseconds(timeout[0] ?? 0, timeout[1] ?? 0, timeout[2] ?? 0);

const default_backup_timout = "00:15:00";
const backup_timeout = (import.meta.env.VITE_BACKUP_INACTIVITY_TIMEOUT ?? default_backup_timout).split(":").map((n) => Number(n));
export const BACKUP_INACTIVITY_TIMEOUT = toMilliseconds(timeout[0] ?? 0, timeout[1] ?? 0, timeout[2] ?? 0);

export const SOF_RESOURCES = [
'Patient',
'AllergyIntolerance',
Expand Down
4 changes: 2 additions & 2 deletions src/lib/sofClient.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import FHIR from 'fhirclient';
import { SOF_PATIENT_RESOURCES, SOF_RESOURCES, LOGOUT_URL } from './config.ts';
import { SOF_PATIENT_RESOURCES, SOF_RESOURCES, LOGOUT_URL, POST_LOGOUT_REDIRECT_URI } from './config.ts';

const patientResourceScope = SOF_PATIENT_RESOURCES.map(resourceType => `patient/${resourceType}.read`);
const resourceScope = patientResourceScope.join(" ");
Expand Down Expand Up @@ -71,7 +71,7 @@ export class SOFClient {
let logoutUrl = LOGOUT_URL;
let idToken = this.client.getState("tokenResponse.id_token");
if (idToken) {
logoutUrl = `${LOGOUT_URL}?id_token_hint=${idToken}&post_logout_redirect_uri=${new URL(this.configuration.redirect_uri).toString()}`;
logoutUrl = `${LOGOUT_URL}?id_token_hint=${idToken}&post_logout_redirect_uri=${new URL(POST_LOGOUT_REDIRECT_URI).toString()}`;
}
return logoutUrl;
}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
if (document.visibilityState === "visible") {
resetInactivityTimer();
}
})
});
});
let isOpen = false;
Expand Down

0 comments on commit ed80f0b

Please sign in to comment.