Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/post logout uri config #20

Merged
merged 5 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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