Search code examples
ruby-on-railssecuritydevisecsrf

can't verify CSRF token authenticity after session expires - Rails + devise + redis


We have an issue with CSRF tokens that started when moving our sessions to Redis. The issue is that users sign-out, and leave the login screen for a long time, e.g. overnight. Then, in the morning, the first login attempt always fails as the form CSRF token is not valid anymore as the redis session was deleted from the server (TTL).

I searched online for a few hours, but not sure what's the right way to go. Adding this file solves the issue:

class SessionsController < Devise::SessionsController
  skip_before_filter :require_no_authentication, only: [:new]
end

but from what I read online, this is a security risk. I kept searching, and saw few alternatives:

  • on failed attempt, return a new token from the server and resubmit the form
  • before submitting the form, make an ajax GET call to retrieve a fresh token
  • add the token to the request headers on the login controller

If I understood the security risk correctly, I don't see how any of those solution solves the security issue. I mean, from what I read the risk of unprotected login API is that an attacker can trick someone else into signing in to the attacker profile and enter private data, which will then be available to the attacker. So, with any of those solutions, the attacker could mimic the same behavior and hack himself in, right?

What's the most secure way to fix this issue?


Solution

  • I fixed it by adding a API to get a new token and using it if the login form was open for a long time. Code:

    app/controllers/my_controller.rb:

    def token
      render json: { token: form_authenticity_token }, status: :ok
    end
    

    signin.html:

    $('form#login').submit(function(e) {
        var that = this;
        e.preventDefault();
        // assuming the session TTL is 30 min
        if (new Date() - window.loginPageRenderedAt > 1800000) {
          $.get('token', function(data) {
            var token = data.token;
            $('input[name=authenticity_token]').val(token)
            that.submit();
          }).fail(function() {
            that.submit();
          })
        } else {
          this.submit();
        }
    }