How to handle an expired CSRF token after a page is left open

I’m using CodeIgniter 2 along with the Ion Auth authorization system by Ben Edmunds.

After creating my project, I would sometimes get a CodeIgniter error upon certain login attempts but this error was intermittent.

The action you have requested is not allowed.

After some troubleshooting, it became apparent this error was caused by an invalid CSRF token. Why is the token invalid? Well, in CodeIgniter’s configuration file, it’s set to expire in 4 hours. So if you load your login page and allow it to sit there for 4 hours before attempting a login, the CSRF tokens will expire and this will generate the error message as above. Simply reloading the login page avoids any issues.

You can verify this error message for yourself by deleting the CSRF cookie after you load the login page.

A cleaner solution would be to redirect to a custom error page or to display a flash message. However, this solution is not as simple as it sounds because when you extend the CodeIgniter Security class, certain hook-points are not available and you cannot yet access CodeIgniter’s Super Object using `get_instance()`.

So when you extend the Security class, you’re limited to standard PHP. In this case, I’m using PHP `header()` to redirect the offending login page (or any form page) back to itself.

<?php
class MY_Security extends CI_Security {

    public function __construct()
    {
        parent::__construct();
    }

    public function csrf_show_error()
    {
        // show_error('The action you have requested is not allowed.');  // default code

        // force page "refresh" - redirect back to itself with sanitized URI for security
        // a page refresh restores the CSRF cookie to allow a subsequent login
        header('Location: ' . htmlspecialchars($_SERVER['REQUEST_URI']), TRUE, 200);
    }
}

This works fine except that the user gets a screen refresh without any indication why they have to enter their login credentials a seconds time.

I decided to make this a bit more user-friendly by adding another function into a Controller, in my case, the Ion Auth controller…

function csrf_redirect()
{
    $flash = 'Session cookie automatically reset due to expired browser session.&nbsp; Please try again.';
    $this->session->set_flashdata('message', $flash);
    redirect('/login', 'location');
}

As you can see, this function sets a flash message telling the user what happened and then redirects them to a fresh instance of the login page.

Session cookie automatically reset due to expired browser session. Please try again.

Instead of using PHP `header()` to redirect a page refresh, redirect to this new Ion Auth controller function at `/auth/csrf_redirect`.

<?php
class MY_Security extends CI_Security {

    public function __construct()
    {
        parent::__construct();
    }

    public function csrf_show_error()
    {
        // show_error('The action you have requested is not allowed.');  // default code

        // force redirect to the csrf_redirect function
        // this gives the user a useful message instructing them to login again
        // while the CSRF cookie is also refreshed to allow a new login
        header('Location: /auth/csrf_redirect', TRUE, 302);
    }
}

The minor downside to this method is that you are always redirected back to the login page rather than a refresh of whatever page/form you’re trying to submit. However, that should be a moot point, since the session cookie expires at nearly the same time as the CSRF cookie, you’d be redirected back to the same login page regardless. You may also not be requiring the user be logged in for your particular form, so please be aware and re-direct accordingly.

When CodeIgniter’s CSRF Protection breaks your Ajax

CSRF stands for “Cross Site Request Forgery” and if you’re using forms on your site, you’ll probably want to protect yourself and users against this kind of attack.

You just finished your latest PHP project using the CodeIgniter framework and decide to enable the CSRF protection option in your `config.php` file.

$config['csrf_protection']  = TRUE;  // <- set to TRUE
$config['csrf_token_name']  = 'csrf_token';   // <- name this whatever
$config['csrf_cookie_name'] = 'csrf_cookie';  // <- name this whatever
$config['csrf_expire'] = 7200;  // <- default is two hours

Enabling it within `config.php` is not enough. You also need to use the form helper `form_open()` function to construct the form's HTML markup. This function constructs the form so that it contains a `<input type="hidden">` element containing the CSRF token value. If the submitted form data is missing this token, it will not submit.

Now CSRF is working but you discover that your jQuery ajax requests are all suddenly failing with a type 500 server error. This is a direct result of activating the CSRF Protection option in CodeIgniter. As just explained, the submitted form data must contain the CSRF token, but it's missing from your ajax requests.

The solution is simple. You need to make sure that your ajax requests simulate a regular form submission by including the CSRF token value within the submitted data.

There are two types of solutions:

Solution #1:

This only works if your ajax requests occur when a form is already constructed on the page, such as when doing remote validation to check to see if a password or username already exists.

You'll need to copy the value from the hidden field called `csrf_token` (the name is exactly as per your `$config['csrf_token_name']` option setting) and send this along with your ajax request.

var csrf = $('input[name="csrf_token"]').val();  // <- get token value from hidden form input

$.ajax({
    // your other ajax options,
    data: {  // submit token value with YOUR token name
        csrf_token: csrf
    }
});

Solution #2:

This works for all ajax requests, even when you do not have a form on the page, such as remotely loading some content.

In this case, you can't get the CSRF token from a hidden field, since there is no form. You must retrieve it from the CSRF cookie. I'm using a jQuery cookie plugin.

var csrf = $.cookie('csrf_cookie');  // <- get token value from cookie

$.ajax({
    // your other ajax options,
    data: {  // submit token value with YOUR token name
        csrf_token: csrf
    }
});

Notice how the ajax in both solutions is sending the token with the same name, that's your name as per your configuration, `csrf_token`. Only the source of the token value is different... Solution #1 gets the token value from the hidden field, where Solution #2 gets the same token value from the cookie.

You can only use Solution #1 when you have a form on the page constructed with the `form_open()` function. However, you can use Solution #2 with or without a form, in all cases.

NOTES:

I have CodeIgniter v2.1.4 and by default, the `$config['csrf_token_name']` option is set to `csrf_test_name`. This mismatch might get a little confusing, but you can use whatever naming convention you wish. In my solution above, I changed it to `csrf_token`.

  • To retrieve the current token from the hidden input, use the name assigned to the `$config['csrf_token_name']` option.
  • To retrieve the current token from the cookie, use the name assigned to the `$config['csrf_cookie_name']` option.

No matter how you retrieve the token value, the important thing to remember is to always send the token value along with whatever name you've assigned to the `$config['csrf_token_name']` option.