Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Devise - Make a request without resetting the countdown until a user is logged out due to inactivity

I'm working on a RoR app with Devise. I want to let clients send a request to the server to see how much time is left until the user on the client is automatically logged out due to inactivity (using the Timeoutable module). I don't want this request to cause Devise to reset the countdown until the user is logged off. How can I configure this?

This is the code I have now:

class SessionTimeoutController < ApplicationController
  before_filter :authenticate_user!

  # Calculates the number of seconds until the user is
  # automatically logged out due to inactivity. Unlike most
  # requests, it should not reset the timeout countdown itself.
  def check_time_until_logout
    @time_left = current_user.timeout_in
  end

  # Determines whether the user has been logged out due to
  # inactivity or not. Unlike most requests, it should not reset the
  # timeout countdown itself.
  def has_user_timed_out
    @has_timed_out = current_user.timedout? (Time.now)
  end

  # Resets the clock used to determine whether to log the user out
  # due to inactivity.
  def reset_user_clock
    # Receiving an arbitrary request from a client automatically
    # resets the Devise Timeoutable timer.
    head :ok
  end
end

SessionTimeoutController#reset_user_clock works because every time RoR receives a request from an authenticated user, Timeoutable#timeout_in is reset to whatever I have configured in Devise#timeout_in. How do I prevent that reset in check_time_until_logout and has_user_timed_out?

like image 788
Kevin Avatar asked Jul 22 '13 15:07

Kevin


2 Answers

I ended up having to make several changes to my code. I'll show what I ended up with when I was finished, then explain what it does:

class SessionTimeoutController < ApplicationController
  # These are what prevent check_time_until_logout and
  # reset_user_clock from resetting users' Timeoutable
  # Devise "timers"
  prepend_before_action :skip_timeout, only: [:check_time_until_logout, :has_user_timed_out]
  def skip_timeout
    request.env["devise.skip_trackable"] = true
  end

  skip_before_filter :authenticate_user!, only: [:has_user_timed_out]

  def check_time_until_logout
    @time_left = Devise.timeout_in - (Time.now - user_session["last_request_at"]).round
  end

  def has_user_timed_out
    @has_timed_out = (!current_user) or (current_user.timedout? (user_session["last_request_at"]))
  end

  def reset_user_clock
    # Receiving an arbitrary request from a client automatically
    # resets the Devise Timeoutable timer.
    head :ok
  end
end

These are the changes I made:

Using env["devise.skip_trackable"]

This is the code that prevents Devise from resetting how long it waits before logging a user out due to inactivity:

  prepend_before_action :skip_timeout, only: [:check_time_until_logout, :has_user_timed_out]
  def skip_timeout
    request.env["devise.skip_trackable"] = true
  end

This code changes a hash value that Devise uses internally to decide whether to update the value it stores to keep track of when the user last had activity. Specifically, this is the Devise code we're interacting with (link):

Warden::Manager.after_set_user do |record, warden, options|
  scope = options[:scope]
  env   = warden.request.env

  if record && record.respond_to?(:timedout?) && warden.authenticated?(scope) && options[:store] != false
    last_request_at = warden.session(scope)['last_request_at']

    if record.timedout?(last_request_at) && !env['devise.skip_timeout']
      warden.logout(scope)
      if record.respond_to?(:expire_auth_token_on_timeout) && record.expire_auth_token_on_timeout
        record.reset_authentication_token!
      end
      throw :warden, :scope => scope, :message => :timeout
    end

    unless env['devise.skip_trackable']
      warden.session(scope)['last_request_at'] = Time.now.utc
    end
  end
end

(Note that this code is executed every time that Rails processes a request from a client.)

These lines near the end are interesting to us:

    unless env['devise.skip_trackable']
      warden.session(scope)['last_request_at'] = Time.now.utc
    end

This is the code that "resets" the countdown until the user is logged out due to inactivity. It's only executed if env['devise.skip_trackable'] is not true, so we need to change that value before Devise processes the user's request.

To do that, we tell Rails to change the value of env['devise.skip_trackable'] before it does anything else. Again, from my final code:

  prepend_before_action :skip_timeout, only: [:check_time_until_logout, :has_user_timed_out]
  def skip_timeout
    request.env["devise.skip_trackable"] = true
  end

Everything above this point is what I needed to change to answer my question. There were a couple other changes I needed to make to get my code to work as I wanted, though, so I'll document them here, too.

Using Timeoutable Correctly

I misread the documentation about the Timeoutable module, so my code in my question has a couple other problems.

First, my check_time_until_logout method was going to always return the same value. This is the incorrect version of the action I had:

def check_time_until_logout
  @time_left = current_user.timeout_in
end

I thought that Timeoutable#timeout_in would return the amount of time until the user was automatically logged out. Instead, it returns the amount of time that Devise is configured to wait before logging users out. We need to calculate how much longer the user has left ourselves.

To calculate this, we need to know when the user last had activity that Devise recognized. This code, from the Devise source we looked at above, determines when the user was last active:

    last_request_at = warden.session(scope)['last_request_at']

We need to get a handle to the object returned by warden.session(scope). It looks like the user_session hash, which Devise provides for us like the handles current_user and user_signed_in?, is that object.

Using the user_session hash and calculating the time remaining ourselves, the check_time_until_logout method becomes

  def check_time_until_logout
    @time_left = Devise.timeout_in - (Time.now - user_session["last_request_at"]).round
  end

I also misread the documentation for Timeoutable#timedout?. It checks to see if the user has timed out when you pass in the time the user was last active, not when you pass in the current time. The change we need to make is straightforward: instead of passing in Time.now, we need to pass in the time in the user_session hash:

  def has_user_timed_out
    @has_timed_out = (!current_user) or (current_user.timedout? (user_session["last_request_at"]))
  end

Once I made these three changes, my controller acted the way I expected it to.

like image 92
Kevin Avatar answered Oct 20 '22 15:10

Kevin


when i see this correctly (probably depending on the devise version) devise handles the logout through the Timeoutable module.

this is hooked up to warden, which is part of the rack middleware stack.

if you look at the code, than you can find this part:

unless warden.request.env['devise.skip_trackable']
  warden.session(scope)['last_request_at'] = Time.now.utc
end

so from what i can see here, you should be able to set devise.skip_trackable to true before the warden middleware get's called.

i think that this issue here explains how to actually use it: https://github.com/plataformatec/devise/issues/953

like image 17
phoet Avatar answered Oct 20 '22 16:10

phoet