Abstract: We are presenting code (JS) for a custom Bootstrap 5 Session Timeout Script. The script logs off the user from the application after the predefined time of inactivity._**
1. The need for the Session Timeout Script
I am working on a Banking application using ASP.NET 8 MVC Bootstrap technology. Requirements regarding security are high, and I am enforcing session timeout on the server side. The problem is if the user is distracted and leaves his workstation and does not log out, and the monitor/browser is still keeping the view/content of some sensitive data. So, I need a script on the client/browser side to hide/remove data from the browser view if the user leaves the workstation for a longer period of time.
I was looking at open-source Bootstrap scripts and found interesting code samples \[1\] and \[2\]. But, I felt I needed to improve them a bit and tailor them to my specific needs. Here is the solution I came up with.
2. Basic Idea
So, here is the basic idea (aka requirements)
- JavaScript/jQuery loaded in the browser with every app form and activated when the user is logged in.
- Script after a predefined time (let’s say 300 sec) of inactivity with the server pops up a dialog asking the user if he wants to stay logged in. If the user does not act (click on the button), the script goes for API/Action Logout and logs out the user of the system. If the user indicates he wants to continue work (click on the button) script goes for API/Action KeepAlive to inform the server side to continue with the session.
- Optionally script can monitor for user activity in the browser, like clicks or mouse moves, not just timeout after no activity against the server
- The dialog should support Bootstrap 5 themes
- Strings presented must support multilingual strings for the EU market
- It can happen that API/Action KeepAlive detects that the user has already been logged out from the session, or the session is expired on the server side. In that case, just redirect to API/Action Logout.
3. Result - Dialogs UI
Here is the result dialog in multiple Bootstrap 5 themes:
![Session expired]()
![Logout]()
![Redirecting]()
4. Problems with jQuery $.ajax
The script is using jQuery $.ajax API. I had a lot of problems with it, so I will explain here. This issue is complex and needs a detailed explanation.
I was using ASP.NET 8 MVC with cookie authentication, which redirects to the login page if authorization fails.
Here are the three separate issues I encountered:
4.1. Issue 1: Redirect to Login Page
When authentication fails, ASP.NET MVC automatically redirects to the login page.
However, during an AJAX call, I did not want this behavior.
What happens
- ASP.NET sends a 302 redirect.
- The browser then requests the login page, which returns a 200 OK response.
The problem:
- $.ajax merges these two requests and only sees the final 200 OK responses.
- As a result, my code doesn't detect the 302 redirects properly.
Solution
In ASP.NET MVC, detect whether the request is an AJAX call and prevent redirection in that case.
4.2. Issue 2: Keep-Alive Response Handling
- $.ajax expects a 200 OK response from keep-alive calls.
- If the server does not return 200 OK, $.ajax treats it as a failure.
Solution
Ensure that the keep-alive method on the server always returns a 200 OK response.
4.3. Issue 3: Suppressed AJAX Header
- $.ajax usually sends the header "X-Requested-With: XMLHttpRequest" to indicate an AJAX call.
- I relied on this header in ASP.NET MVC to differentiate AJAX requests and prevent redirects on failure.
- However, this header may be suppressed in some cases, such as when using CORS with \`crossDomain: true\` in $.ajax.
Solution
Ensure the client is properly configured to retain the "X-Requested-With" header when necessary.
5. Problems with async/defer loading of JS scripts
App I was working on loading a number of scripts, and I was using a “defer” attribute, which is a form of asynchronous loading behavior. The problem is that you can not know which one is loaded and when, so I needed to improve the scripts to anticipate/wait for jQuery to be loaded. That is done at two places:
- In the script itself, waiting for jQuery to load
- The initialization of the script is waiting for the "DOMContentLoaded" event
6. The Code
Here is the code. It has a reasonable amount of comments, so one can understand how it works.
6.1. The JS script
This is the JS script itself, you need to load it and activate it.
// Written by Mark Pelf - 2025
// inspired by code from:
// https://github.com/maxfierke/jquery-sessionTimeout-bootstrap
// https://github.com/Techsolutionstuff/Bootstrap-Session-Timeout-Example
// but I changed it to my needs
function sessionTimeoutInitialization(options) {
'use strict';
//debugger;
//here is START POINT
//we do this because we need to wait for jQuery to load
//jQuery can have defer attribute in script tag
let waitForJQuery = setInterval(function () {
if (typeof $ != 'undefined') {
clearInterval(waitForJQuery);
sessionTimeout(options);
}
}, 50);
//----functions-----------------------------------------
function sessionTimeout(options) {
let defaults = {
isUserAuthenticatedFlag: false,
dialog1_titleText: 'Your Session is About to Expire!',
dialog1_messageText: 'Your session is about to expire.',
dialog1_logoutButtonText: 'Logout',
dialog1_keepAliveButtonText: 'Stay Connected',
dialog1_keepAliveUrl: '/keep-alive',
dialog1_ajaxType: 'GET',
dialog1_ajaxData: '',
dialog1_redirUrl: '/timed-out',
dialog1_logoutUrl: '/log-out',
dialog1_warnAfterMiliSeconds: 900000, // 15 minutes
dialog1_redirAfterMiliSeconds: 1200000, // 20 minutes
ignoreUserMouseKeyboardActivityDuringWaitingFlag: false,
dialog1_countdownMessageText: 'Redirecting in {timer} seconds.',
dialog1_countdownBarFlag: false,
};
let opt = defaults;
if (options) {
opt = $.extend(defaults, options);
}
//debugger;
//only start monitoring for timeout if user is authenticated
if (opt.isUserAuthenticatedFlag) {
let sessionTimer; //variable to hold session timer
let countdownDialog1Object = {}; //object to hold countdown timer and variables
let dialog1Timer; //variable to hold dialog timer
let dialogCreatedFlag = false; //flag to indicate if dialog is created
let timestampLastUserActivity = Date.now();
// check proper configuration
if (opt.dialog1_warnAfterMiliSeconds >= opt.dialog1_redirAfterMiliSeconds) {
console.error('Bootstrap-session-timeout plugin is miss-configured. Option '+
'"dialog1_redirAfterMiliSeconds" must be equal or greater than "dialog1_warnAfterMiliSeconds".');
return false;
}
{ // this is just for block scope
// countdownMessage string
let countdownMessage = opt.dialog1_countdownMessageText ?
'<p>' + opt.dialog1_countdownMessageText.replace(
/{timer}/g, '<span class="countdown-holder"></span>') + '</p>' : '';
// countdownBar HTML-string
let coundownBarHtml = opt.dialog1_countdownBarFlag ?
'<div class="progress"> \
<div class="progress-bar progress-bar-striped countdown-bar active" \
role = "progressbar" style = "min-width: 15px; width: 100%;" > \
<span class="countdown-holder"></span> \
</div> \
</div>'
: '';
//create modal dialog, id="session-timeout-dialog"
$('body').append(
'<div class="modal fade" id="session-timeout-dialog" data-bs-backdrop="static" tabindex="-1"> \
<div class="modal-dialog modal-dialog-centered"> \
<div class="modal-content"> \
<div class="modal-header text-bg-secondary"> \
<h4 class="modal-title ">' + opt.dialog1_titleText + '</h4> \
</div> \
<div class="modal-body"> \
<p>' + opt.dialog1_messageText + '</p> \
' + countdownMessage + ' \
' + coundownBarHtml + ' \
</div> \
<div class="modal-footer"> \
<button id="session-timeout-dialog-logout" type="button" \
class= "btn btn-primary" > '
+ opt.dialog1_logoutButtonText + '</button> \
<button id="session-timeout-dialog-keepalive" type="button" \
class="btn btn-primary" data-dismiss="modal">'
+ opt.dialog1_keepAliveButtonText + '</button> \
</div> \
</div> \
</div> \
</div>');
//user clicked logout -
$('#session-timeout-dialog-logout').on('click',
logoutUserHandler
);
//user clicked keep-alive - wants to stay logged in
$('#session-timeout-dialog-keepalive').on('click',
stayLoggedinHandler
);
//monitor for user keyboard and mouse activity
if (!opt.ignoreUserMouseKeyboardActivityDuringWaitingFlag) {
let mousePosition = [-1, -1];
//here we hook to document for user activity events
$(document).on('keyup mouseup mousemove touchend touchmove',
// function to do in case of user activity
function (e) {
if (e.type === 'mousemove') {
if (e.clientX === mousePosition[0] && e.clientY === mousePosition[1]) {
return;
}
mousePosition[0] = e.clientX;
mousePosition[1] = e.clientY;
timestampLastUserActivity = Date.now();
};
}
);
}
//invoke main work - checkAndRestartSessionTimerAndWait()
checkAndRestartSessionTimerAndWait();
}// end of block scope
function logoutUserHandler() {
timestampLastUserActivity = Date.now();
clearTimeout(sessionTimer);
clearTimeout(dialog1Timer);
closeDialog();
window.location = opt.dialog1_logoutUrl;
}
function stayLoggedinHandler() {
timestampLastUserActivity = Date.now();
closeDialog();
//renew session
callKeepAliveOnceAndPerformAction(
checkAndRestartSessionTimerAndWait, logoutUserHandler);
}
function closeDialog() {
//debugger;
if (dialogCreatedFlag) {
clearTimeout(countdownDialog1Object.timer);
clearTimeout(sessionTimer);
clearTimeout(dialog1Timer);
//close modal dialog if opened
let dialog = $('#session-timeout-dialog');
dialog.hide();
dialog.modal('hide');
//$('body').removeClass('modal-open');
//$('div.modal-backdrop').remove();
dialogCreatedFlag = false;
}
}
function callKeepAliveOnceAndPerformAction(successFunction, failureFunction) {
// IMPORTANT: VERY STRANGE BEHAVIUR OF $.ajax
// Here we have such a nasty situation that I need to explain
// I was using ASP.NET8 MVC and was having cookie authenticate
// with redirect to login page if authorize failed
// there are 3 separate issues:
// 1. ASP.NET MVC was doing redirect to login page, when I didn't want it to do (during ajax call)
// 2. $.ajax is expecting 200 OK from your keep-alive call
// 3. Sometimes AJAX header "X-Requested-With: XMLHttpRequest" is suppressed
// So, here is explanation:
// Problem 1: ------------------------
// If authentication fails, ASP.NET MVC was doing redirect to login page
// ASP.NET MVC was doing redirect to login page, when I didn't want it to do
// what it does is 1)send 302-redirect and then 2)ends login page 200
// $.ajax would accumulate those 2 calls into one (it thinks it is smarter than you)
// so I would not get 302, but 200, which would trick my code
// The solution for this is to set in ASP.NET MVC code to differentiate between
// ajax and non-ajax calls and not to do redirect in case of ajax call failure
// Problem 2: ------------------------
// $.ajax is expecting 200 OK from your keep-alive call
// or otherwise it will treat it as failure
// so, you need to return 200 OK from your keep - alive call
// from server side inside your keep-alive call method
// Problem 3: ------------------------
// Sometimes $.ajax header "X-Requested-With: XMLHttpRequest" is suppressed
// I was using that header to detect in APS.NET MVC if it is an ajax call
// and not to do redirect in case of failure
// but sometimes that header is suppressed, for example in case of CORS
// with option "crossDomain: true" in $.ajax
// So, your client needs to be configured properly
//-------------------------------------------------
//debugger;
$.ajax({
type: opt.dialog1_ajaxType,
url: opt.dialog1_keepAliveUrl,
data: opt.dialog1_ajaxData,
//be very carefull with this property
//it will suppress AJAX header "X-Requested-With: XMLHttpRequest"
//crossDomain: true, // this is for CORS
xhrFields: {
withCredentials: true
}
})
.then(
// success
function (data, textStatus, jqXHR) {
//debugger;
//alert("KeepAlive request success " + jqXHR.statusText);
successFunction();
},
// failure
function (data, textStatus, jqXHR) {
//debugger;
//alert("KeepAlive request failure " + jqXHR.statusText);
failureFunction();
}
);
}
//check for user acitivity and restart session timer ======================
function checkAndRestartSessionTimerAndWait() {
clearTimeout(sessionTimer);
let timeNowMiliSeconds = Date.now();
let timeElapsed = timeNowMiliSeconds - timestampLastUserActivity;
let timeToWait = opt.dialog1_warnAfterMiliSeconds - timeElapsed;
//check time to wait
if (timeToWait > 0) {
sessionTimer = setTimeout(
//function to do after timer expires
function () {
//check again
checkAndRestartSessionTimerAndWait();
},
//time to wait until next check
timeToWait
);
}
else {
//time to wait has expired
//show modal dialog
createDialog1();
}
}
//createDialog1 =======================================================
function createDialog1() {
if (!dialogCreatedFlag) {
dialogCreatedFlag = true;
clearTimeout(sessionTimer);
$('#session-timeout-dialog').modal('show');
startDialog1CountdownTimer(true);
//this is if user does not click a any button
//we logout user
let durationOfDialogMiliSeconds =
opt.dialog1_redirAfterMiliSeconds - opt.dialog1_warnAfterMiliSeconds;
dialog1Timer = setTimeout(
//function to do after dialog timer
logoutUserHandler,
//time how long to show dialog
durationOfDialogMiliSeconds
);
}
}
//start dialog1 countdown timer =========================================
// we have 2 timers for dialog, one is for dialog itself and other is for countdown
// because CountdownTimer is optional, we can not integrate them into one
function startDialog1CountdownTimer(startCountingFromBeginningFlag) {
clearTimeout(countdownDialog1Object.timer);
let durationOfDialogMiliSeconds = opt.dialog1_redirAfterMiliSeconds - opt.dialog1_warnAfterMiliSeconds;
if (startCountingFromBeginningFlag) {
countdownDialog1Object.timeLeftSeconds =
Math.floor((durationOfDialogMiliSeconds) / 1000);
}
if (opt.dialog1_countdownBarFlag) {
countdownDialog1Object.percentLeft =
Math.floor(countdownDialog1Object.timeLeftSeconds / ((durationOfDialogMiliSeconds) / 1000) * 100);
}
let countdownEl = $('.countdown-holder');
let secondsLeft = countdownDialog1Object.timeLeftSeconds >= 0 ? countdownDialog1Object.timeLeftSeconds : 0;
{
let minLeft = Math.floor(secondsLeft / 60);
let secRemain = secondsLeft % 60;
let countText = minLeft > 0 ? minLeft + 'm' : '';
if (countText.length > 0) {
countText += ' ';
}
countText += secRemain + 's';
countdownEl.text(countText);
}
if (opt.dialog1_countdownBarFlag) {
$('.countdown-bar').css('width', countdownDialog1Object.percentLeft + '%');
}
countdownDialog1Object.timeLeftSeconds = countdownDialog1Object.timeLeftSeconds - 1;
if (countdownDialog1Object.timeLeftSeconds > 0) {
//self start countdown timer again after 1 second
countdownDialog1Object.timer =
setTimeout(
function () {
startDialog1CountdownTimer(false);
},
//1000 miliseconds
1000
);
}
else {
//the end, user did not click any button to stay logedin
logoutUserHandler();
}
}//end of startDialog1CountdownTimer
}
} // function sessionTimeout(options)
}//function sessionTimeoutInitialization
```
6.2. C#/MVC activation code
This is code from my APS.NET 8 MVC app, from \_Layout.cshtml, where I activate and configure the script.
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" asp-append-version="true" />
<script src="~/lib/jquery/dist/jquery.min.js" defer asp-append-version="true"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js" defer asp-append-version="true"></script>
<script src="~/lib/bootstrap-session-timeout/bootstrap-session-timeout.js" defer asp-append-version="true"></script>
<!--Here we setup sessionTimeout ----------------------->
<script>
//debugger;
document.addEventListener("DOMContentLoaded", function() {
// we go here for "DOMContentLoaded"
// because we load script with defer (async load)
sessionTimeoutInitialization({
isUserAuthenticatedFlag: @ViewBag.isUserAuthenticatedFlag.ToString().ToLowerInvariant() ,
dialog1_titleText: '@ViewBag.dialog1_titleText',
dialog1_messageText: '@ViewBag.dialog1_messageText',
dialog1_logoutButtonText: '@ViewBag.dialog1_logoutButtonText',
dialog1_keepAliveButtonText: '@ViewBag.dialog1_keepAliveButtonText',
dialog1_keepAliveUrl: '@ViewBag.dialog1_keepAliveUrl',
dialog1_redirUrl: '@ViewBag.dialog1_redirUrl',
dialog1_logoutUrl: '@ViewBag.dialog1_logoutUrl',
dialog1_warnAfterMiliSeconds: '@ViewBag.dialog1_warnAfterMiliSeconds',
dialog1_redirAfterMiliSeconds: '@ViewBag.dialog1_redirAfterMiliSeconds',
dialog1_countdownMessageText: '@ViewBag.dialog1_countdownMessageText',
dialog1_countdownBarFlag: @ViewBag.dialog1_countdownBarFlag.ToString().ToLowerInvariant()
});
});
</script>
```
Conclusion
Honestly, I tested only the setups that are needed for my application, but I made it work reliably for me. Maybe some more testing will be good for your specific case.
References
[1] https://github.com/maxfierke/jquery-sessionTimeout-bootstrap
[2] https://github.com/Techsolutionstuff/Bootstrap-Session-Timeout-Example