From d73c16efa26921e5175ea48c3466c313abfa8487 Mon Sep 17 00:00:00 2001 From: nemo Date: Mon, 20 Nov 2023 22:52:20 +0000 Subject: [PATCH] Add auto-logout function. Add an auto logout function that makes a call to the /obp/v5.1.0/ui/suggested-session-timeout to find the suggested session timeout time. Failing this, we default to a timeout of 300 seconds. A timer can be seen next to the logout button which shows the time remaining before logout. --- apimanager/apimanager/settings.py | 30 ++--- apimanager/base/static/js/inactivity-timer.js | 39 +++++++ apimanager/base/static/js/inactivity.js | 107 ++++++++++++++++++ apimanager/base/templates/base.html | 9 +- 4 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 apimanager/base/static/js/inactivity-timer.js create mode 100644 apimanager/base/static/js/inactivity.js diff --git a/apimanager/apimanager/settings.py b/apimanager/apimanager/settings.py index 851afe3d..a807a803 100644 --- a/apimanager/apimanager/settings.py +++ b/apimanager/apimanager/settings.py @@ -89,20 +89,6 @@ # 'django.middleware.cache.FetchFromCacheMiddleware', ] -# Content Security Policy - External Urls for scripts, styles, and images should be included here -#TODO these outside scripts should really just be loaded when we run "manage.py collectstatic" -# Or the whole static folder could be uploaded to github, this prevents API manager breaking when -# we run it on a server that may not connect to these sites - -# Inline styles loaded by jsoneditor.min.js have been allowed by adding their hashes to CSP_STYLE_SRC - -CSP_IMG_SRC = ("'self' data:", 'https://static.openbankproject.com') -CSP_STYLE_SRC = ("'self' 'sha256-z2a+NIknPDE7NIEqE1lfrnG39eWOhJXWsXHYGGNb5oU=' 'sha256-Dn0vMZLidJplZ4cSlBMg/F5aa7Vol9dBMHzBF4fGEtk=' 'sha256-sA0hymKbXmMTpnYi15KmDw4u6uRdLXqHyoYIaORFtjU=' 'sha256-jUuiwf3ITuJc/jfynxWHLwTZifHIlhddD8NPmmVBztk=' 'sha256-RqzjtXRBqP4i+ruV3IRuHFq6eGIACITqGbu05VSVXsI='", 'https://cdnjs.cloudflare.com', ) -CSP_SCRIPT_SRC = ("'self' 'sha256-4Hr8ttnXaUA4A6o0hGi3NUGNP2Is3Ep0W+rvm+W7BAk=' 'sha256-GgQWQ4Ejk4g9XpAZJ4YxIgZDgp7CdQCmqjMOMh9hD2g=' 'sha256-05NIAwVBHkAzKcXTfkYqTnBPtkpX+AmQvM/raql3qo0='", 'http://code.jquery.com', 'https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/', 'https://cdnjs.cloudflare.com') -CSP_FONT_SRC = ("'self'", 'http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/') -CSP_FRAME_ANCESTORS = ("'self'") -CSP_FORM_ACTION = ("'self'") - #cache the view page, we set 60s = 1m, # CACHE_MIDDLEWARE_SECONDS = 60 @@ -353,3 +339,19 @@ raise ImproperlyConfigured('Missing settings for OAUTH_CONSUMER_KEY') if not OAUTH_CONSUMER_SECRET: raise ImproperlyConfigured('Missing settings for OAUTH_CONSUMER_SECRET') + +#This has been moved to after API_HOST is imported so that connections to the API are allowed by the csp +# Content Security Policy - External Urls for scripts, styles, and images should be included here +#TODO these outside scripts should really just be loaded when we run "manage.py collectstatic" +# Or the whole static folder could be uploaded to github, this prevents API manager breaking when +# we run it on a server that may not connect to these sites + +# Inline styles loaded by jsoneditor.min.js have been allowed by adding their hashes to CSP_STYLE_SRC + +CSP_IMG_SRC = ("'self' data:", 'https://static.openbankproject.com') +CSP_STYLE_SRC = ("'self' 'sha256-z2a+NIknPDE7NIEqE1lfrnG39eWOhJXWsXHYGGNb5oU=' 'sha256-Dn0vMZLidJplZ4cSlBMg/F5aa7Vol9dBMHzBF4fGEtk=' 'sha256-sA0hymKbXmMTpnYi15KmDw4u6uRdLXqHyoYIaORFtjU=' 'sha256-jUuiwf3ITuJc/jfynxWHLwTZifHIlhddD8NPmmVBztk=' 'sha256-RqzjtXRBqP4i+ruV3IRuHFq6eGIACITqGbu05VSVXsI='", 'https://cdnjs.cloudflare.com', ) +CSP_SCRIPT_SRC = ("'self' 'sha256-4Hr8ttnXaUA4A6o0hGi3NUGNP2Is3Ep0W+rvm+W7BAk=' 'sha256-GgQWQ4Ejk4g9XpAZJ4YxIgZDgp7CdQCmqjMOMh9hD2g=' 'sha256-05NIAwVBHkAzKcXTfkYqTnBPtkpX+AmQvM/raql3qo0='", 'http://code.jquery.com', 'https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/', 'https://cdnjs.cloudflare.com') +CSP_FONT_SRC = ("'self'", 'http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/') +CSP_FRAME_ANCESTORS = ("'self'") +CSP_FORM_ACTION = ("'self'") +CSP_CONNECT_SRC = (API_HOST) diff --git a/apimanager/base/static/js/inactivity-timer.js b/apimanager/base/static/js/inactivity-timer.js new file mode 100644 index 00000000..2ce6dd06 --- /dev/null +++ b/apimanager/base/static/js/inactivity-timer.js @@ -0,0 +1,39 @@ +function addSeconds(date, seconds) { + let oldDate = date; + let addSeconds = seconds; + var newSeconds = oldDate + addSeconds; + //console.log(addSeconds); + date.setSeconds(date.getSeconds() + seconds); + return date; +} + +export function showCountdownTimer() { + + // Get current date and time + var now = new Date().getTime(); + let distance = countDownDate - now; + // Output the result in an element with id="countdown-timer-span" + let elementId = ("countdown-timer-span"); + document.getElementById(elementId).innerHTML = "in " + Math.floor(distance / 1000) + "s"; + + // If the count down is over release resources + if (distance < 0) { + destroyCountdownTimer(); + } +} + + +// Set the date we're counting down to +let countDownDate = addSeconds(new Date(), 5); + +let showTimerInterval = null; + +export function destroyCountdownTimer() { + clearInterval(showTimerInterval); +} + +export function resetCountdownTimer(seconds) { + destroyCountdownTimer(); // Destroy previous timer if any + countDownDate = addSeconds(new Date(), seconds); // Set the date we're counting down to + showTimerInterval = setInterval(showCountdownTimer, 1000); // Update the count down every 1 second +} \ No newline at end of file diff --git a/apimanager/base/static/js/inactivity.js b/apimanager/base/static/js/inactivity.js new file mode 100644 index 00000000..84b14a16 --- /dev/null +++ b/apimanager/base/static/js/inactivity.js @@ -0,0 +1,107 @@ +import * as countdownTimer from './inactivity-timer.js' + +// holds the idle duration in ms (current value = 301 seconds) +var timeoutIntervalInMillis = 5 * 60 * 1000 + 1000; +// holds the timeout variables for easy destruction and reconstruction of the setTimeout hooks +var timeHook = null; + +function initializeTimeHook() { + // this method has the purpose of creating our timehooks and scheduling the call to our logout function when the idle time has been reached + if (timeHook == null) { + timeHook = setTimeout( function () { destroyTimeHook(); logout()}.bind(this), timeoutIntervalInMillis); + } +} + +function destroyTimeHook() { + // this method has the sole purpose of destroying any time hooks we might have created + clearTimeout(timeHook); + timeHook = null; +} + +function resetTimeHook(event) { + // this method replaces the current time hook with a new time hook + destroyTimeHook(); + initializeTimeHook(); + countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000); + // show event type, element and coordinates of the click + // console.log(event.type + " at " + event.currentTarget); + // console.log("Coordinates: " + event.clientX + ":" + event.clientY); + console.log("Reset inactivity of a user"); +} + +function setupListeners() { + // here we setup the event listener for the mouse click operation + document.addEventListener("click", resetTimeHook); + document.addEventListener("mousemove", resetTimeHook); + document.addEventListener("mousedown", resetTimeHook); + document.addEventListener("keypress", resetTimeHook); + document.addEventListener("touchmove", resetTimeHook); + console.log("Listeners for user inactivity activated"); +} + +function destroyListeners() { + // here we destroy event listeners for the mouse click operation + document.removeEventListener("click", resetTimeHook); + document.removeEventListener("mousemove", resetTimeHook); + document.removeEventListener("mousedown", resetTimeHook); + document.removeEventListener("keypress", resetTimeHook); + document.removeEventListener("touchmove", resetTimeHook); + console.log("Listeners for user inactivity deactivated"); +} + +function logout() { + destroyListeners(); + countdownTimer.destroyCountdownTimer(); + console.log("Logging you out due to inactivity.."); + const logoffButton = document.getElementById("logout"); + logoffButton.click(); +} + +async function makeObpApiCall() { + let timeoutInSeconds; + try { + let obpApiHost = document.getElementById("api_home_link"); + if(obpApiHost) { + obpApiHost = obpApiHost.href.split("?")[0]; + } + console.log(obpApiHost); + const response = await fetch(`${obpApiHost}/obp/v5.1.0/ui/suggested-session-timeout`); + const json = await response.json(); + if(json.timeout_in_seconds) { + timeoutInSeconds = json.timeout_in_seconds; + console.log(`Suggested value ${timeoutInSeconds} is used`); + } else { + timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds + console.log(`Default value ${timeoutInSeconds} is used`); + } + } catch (e) { + console.error(e); + timeoutInSeconds = 5 * 60 + 1; // Set default value to 301 seconds, even if the session timeout endpoint is not reachable for whatever reason + console.log(`Default value ${timeoutInSeconds} is used`); + } + return timeoutInSeconds; +} + +async function getSuggestedSessionTimeout() { + if(!sessionStorage.getItem("suggested-session-timeout-in-seconds")) { + let timeoutInSeconds = await makeObpApiCall(); + sessionStorage.setItem("suggested-session-timeout-in-seconds", timeoutInSeconds); + } + return sessionStorage.getItem("suggested-session-timeout-in-seconds") * 1000 + 1000; // We need timeout in millis +} + +// self executing function to trigger the operation on page load +(async function () { + timeoutIntervalInMillis = await getSuggestedSessionTimeout(); // Try to get suggested value + const logoffButton = document.getElementById("countdown-timer-span"); + if(logoffButton) { + // to prevent any lingering timeout handlers preventing memory leaks + destroyTimeHook(); + // setup a fresh time hook + initializeTimeHook(); + // setup initial event listeners + setupListeners(); + // Reset countdown timer + countdownTimer.resetCountdownTimer(timeoutIntervalInMillis / 1000); + } +})(); \ No newline at end of file diff --git a/apimanager/base/templates/base.html b/apimanager/base/templates/base.html index e4929480..da7cdde7 100644 --- a/apimanager/base/templates/base.html +++ b/apimanager/base/templates/base.html @@ -31,7 +31,7 @@