1

I am working on a simple project where I have a login system with multiple users where each user will login and save form entries in a file. I am trying to build session timeout so that after 2 minutes of inactivity it can log me out.

  • I have login.php page where I provide my user and password.
  • If login is successful then it redirects me to index.php page where I have form with two textbox and a button.
  • On index.php page I have Save Data button which if I click it calls save.php then it save form entries by overwriting in file.
  • I also have logout link on my index.php page which if I click then it will log me out and redirect to login.php page.

All above things works fine. Now I am trying to build this session timeout so that it can log me out after x minutes of inactivity and redirect me to login.php page.

Here is my index.php file:

<?php

declare(strict_types = 1);

// Start session.
session_start();

// Include helper functions.
require_once 'helpers.php';

// 2 mins in seconds
$inactive = 120; 

if(isset($_SESSION['timeout']) ) {
    $session_life = time() - $_SESSION['timeout'];
    if($session_life > $inactive)
    { 
        redirect('logout.php');
        return;
    }
}

$_SESSION['timeout'] = time();

// Redirect user to login page if not authenticated.
if (! check_auth()) {
    redirect('login.php');
    return;
}

?>
<!doctype html>
<html>
<head>
    <title>Home</title>
</head>
<body>
    <div>
        <h1>Website Title</h1> <a href="logout.php">Logout</a> </div>
    <div>
        <p>Welcome back, <?= $_SESSION['user_id'] ?>!</p>
    </div>
    <form method="post">
        <input type="text" name="field1" />
        <input type="text" name="field2" />
        <input type="submit" name="submit" value="Save Data"> </form>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script>
    $(function() {
        $('form').submit(function(e) {
            e.preventDefault();
            $.post({
                url: 'save.php',
                data: $(this).serialize(),
            }).done(response => {
                response = JSON.parse(response);
                if(response.message) {
                    alert(response.message);
                }
            });
        });
    });
    </script>
</body>
</html>

Here is my save.php file:

<?php
declare(strict_types=1);
// Start session.
session_start();
// Include helper functions.
require_once 'helpers.php';

// 2 mins in seconds
$inactive = 120; 

if(isset($_SESSION['timeout']) ) {
    $session_life = time() - $_SESSION['timeout'];
    if($session_life > $inactive)
    { 
        redirect('logout.php');
        return;
    }
}

$_SESSION['timeout'] = time();

// Redirect user to login page if not authenticated.
if (! check_auth()) {
    redirect('login.php');
    return;
}

// save form entries in a file

Here is my helpers.php file:

<?php
declare(strict_types=1);

if (! function_exists('check_auth')) {
    function check_auth(): bool
    {
        return isset($_SESSION['user_id']);
    }
}

if (! function_exists('logout'))
{
    function logout()
    {
        if (isset($_SESSION['user_id'])) {
            $trace = json_decode(file_get_contents('current_user.txt'));

            if ((int) $trace->user_id === $_SESSION['user_id']) {
                $content = isset($trace->revoked_user_id)
                ? json_encode(['user_id' => $trace->revoked_user_id, 'created_at' => (new DateTime('now'))->format('Y-m-d H:i:s')])
                : '';
                file_put_contents('current_user.txt', $content);
            }

            unset($_SESSION['user_id']);
            unset($_SESSION['timeout']);
        }
    }
}

if (! function_exists('redirect')) {
    function redirect(string $url, int $status_code = 303): void
    {
        header('Location: ' . $url, true, $status_code);
        die();
    }
}

Problem Statement

I am redirecting to logout.php file once it is idle for 2 minutes in index.php and save.php. Here is what I am noticing -

  • If I am on index.php page and after 2 minutes of inactivity, if I refresh my browser then it logs me out fine without any issues and redirects me to login.php page.
  • Now let's say If I am on index.php page again and after 2 minutes of inactivity if I click on Save Data button then it gives me error on my console but techincally it should have logged me out and redirected me to login.php page.

Below is the ajax call I have in my index.php page -

<script>
$(function() {
    $('form').submit(function(e) {
        e.preventDefault();
        $.post({
            url: 'save.php',
            data: $(this).serialize(),
        }).done(response => {
            response = JSON.parse(response);
            if(response.message) {
                alert(response.message);
            }
        });
    });
});
</script>

I get a json parsing error for my point 2 whenever I click Save Data button after 2 minutes of inactivity. And value of response variable is full login.php in html. I am not sure why it is happening. Do we need to check for session validation in jquery itself before it calls save.php?

David Todd
  • 63
  • 5
  • Hi, interesting, perhaps use PHP's built-in session timeout. This might be of interest https://stackoverflow.com/questions/520237/how-do-i-expire-a-php-session-after-30-minutes – IronMan Sep 29 '20 at 23:15
  • I also tried that and same thing happens if I click `Save Data` button I get an error with same invalid json error as I mentioned. I think it is something I need to do in my ajax call to check for session validity before calling save.php file maybe? – David Todd Sep 29 '20 at 23:28
  • Does this answer your question? [How to manage a redirect request after a jQuery Ajax call](https://stackoverflow.com/questions/199099/how-to-manage-a-redirect-request-after-a-jquery-ajax-call) – Will B. Sep 29 '20 at 23:51

1 Answers1

2

In summary, your issue is caused by the redirect occurring in save.php when it is being requested by ajax.

What happens is the redirect() request is processed transparently and the results are being processed by the jQuery.ajax().done() closure, which is trying to call JSON.parse(response); for the HTML of login.php. You should be able to validate this by viewing the developer tools (usually F12) in your browser, when clicking the button.

The general approach to resolve the issue, is to determine if the request is a XMLHttpRequest and send a different response instead of redirecting.

Detect XMLHttpRequest

To determine if the request is coming from jQuery.ajax(), you would check the X-Requested-With request header.

isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'

JSON Response with redirect property

This approach returns a redirect property in the response and uses it to redirect from the jQuery.ajax().done() closure.

helpers.php

if (! function_exists('redirect')) {
    function redirect(string $url, int $status_code = 303): void
    {
        if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
            //do not redirect AJAX requests
            echo json_encode(['redirect' => $url]);
        } else {
            header('Location: ' . $url, true, $status_code);
        }
        die();
    }
}

index.php

$(function() {
    "use strict";
    $('form').submit(function(e) {
        e.preventDefault();
        $.post({
            url: 'save.php',
            data: $(this).serialize(),
        }).done(response => {
            response = JSON.parse(response);
            if (response.redirect) {
                //redirect user
                window.location.href = response.redirect; 
            }
            if (response.message) {
                alert(response.message);
            }
        });
    });
});

Status Code Response

This approach returns a status code other than 200 Ok, instead of redirecting and checks for the response status code in the jQuery.ajax() or statusCode: method(s).

helpers.php

if (! function_exists('redirect')) {
    function redirect(string $url, int $status_code = 303): void
    {
        if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
            //do not redirect AJAX requests
            http_response_code(401); //Unauthorized - PHP 5.4+
            echo $url; //optionally output the url
        } else {
            header('Location: ' . $url, true, $status_code);
        }
        die();
    }
}

Here you can choose to handle the status code as desired. For the simplest approach, checking the fail() jqXhr object status code and perform a javascript redirect instead.

index.php

$(function() {
    "use strict";
    $('form').submit(function(e) {
        e.preventDefault();
        $.post({
            url: 'save.php',
            data: $(this).serialize(),
        }).done(response => {
            response = JSON.parse(response);
            if (response.message) {
                alert(response.message);
            }
        }).fail(jqXhr => {
            if (jqXhr.status == 401) {
                //redirect to specified url in the response text
                window.location.href = jqXhr.responseText; 
               /* alternatively hard-code '/logout.php' */
            }
        });
    });
});
Will B.
  • 17,883
  • 4
  • 67
  • 69
  • Interesting. I wasn't aware of that at all. Thanks for teaching me this. Appreciate your help! I see this variable is not defined `sapi_type`? Do we need to define somewhere? Sorry this is kinda new to me. – David Todd Sep 30 '20 at 00:13
  • @DavidTodd Updated to remove the CGI conditional check, since you're using PHP > 5.4 – Will B. Sep 30 '20 at 00:14
  • Thanks Will. There is one very weird issue I am seeing. I have this code running on my company server and for the first time if you open the website link on browser then it asks for user and password (which is needed to get in) and then subsequent links if you open it doesn't ask anything and it stays like that but after this change what I am noticing is - if I refresh my browser after X minutes of inactivity then it logs out which is fine but if I click "Save Data" button after X minutes of inactivity then it asks same username and password that I typed for the first time for my website. – David Todd Sep 30 '20 at 21:46
  • .. And then once I type the user and password it logs out and then it takes me to login screen. So my confusion is why it behaves weirdly on `Save Data` button click. Any thoughts? – David Todd Sep 30 '20 at 21:47
  • The JS redirect should act as if the user is navgating to `/logout.php` directly, where is the redirect going? I would need to see your login code logic to determine what is actually happening. Please post another question with the new issue and your updated code using this solution, but sounds like the session state was not cleared properly in `/logout.php`. – Will B. Sep 30 '20 at 22:05
  • Sure I will open a new question for that. Quick question how can I check where the redirect is going? In the `developer tools` it will show? My logout.php is already mentioned in the question and it call `logout` method in `helpers.php` file. – David Todd Sep 30 '20 at 22:16
  • Also as another helpful tip. You should not be redirecting to `/login.php` if `check_auth()` fails. This will leave your session in an invalid state with `$_SESSION['timeout']` still defined. Use `/logout.php` instead, to ensure the session state is cleared before redirecting to `/login.php`. I also strongly advise looking into [session hi-jacking](https://stackoverflow.com/questions/5081025/php-session-fixation-hijacking) prevention techniques. – Will B. Sep 30 '20 at 22:18
  • Ahh.. Thanks for the tip. I will fix that as well and open a new question for the current issue I am seeing. Appreciate your help! – David Todd Sep 30 '20 at 22:19
  • Regarding dev tools. In Chrome/Firefox, Open the Dev Tools (F12) and click the *Network* tab and ensure in Chrome the *Preserve Log* or in FireFox click the Gear Icon and the *Persist Logs* is enabled. This will show the request and response log with the redirects, something like https://i.imgur.com/0RUBUY9.png – Will B. Sep 30 '20 at 22:34