Bootstrap5 Session Timeout Script (JS)

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

  1. ASP.NET sends a 302 redirect.
  2. 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

Up Next
    Ebook Download
    View all
    Learn
    View all