Ruby Yagi šŸ

Ruby, Rails, Web dev articles

Devise only allow one session per user at the same time

by Axel Kee 16 January 2021

Sometimes for security purpose, or you just dont want users to share their accounts, it is useful to implement a check to ensure that only one login (session) is allowed per user at the same time.

To check if there is only one login, we will need a column to store information of the current login information. We can use a string column to store a token, and this token will be randomly generated each time the user is logged in, and then we compare if the current sessionā€™s login token is equal to this token.

Assuming your users info are stored in the users table (you can change to other name for the command below), create a ā€œcurrent_login_tokenā€ column for the table :

rails g migration AddCurrentLoginTokenToUsers current_login_token:string

This would generate migration file below :

class AddCurrentLoginTokenToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :current_login_token, :string
  end
end

Now run rails db:migrate to run this migration.

rails db:migrate

Next, we would need to override the create method of SessionsController from Devise, so that we can generate a random string for the current_login_token column each time the user signs in.

If you donā€™t already have a custom SessionsController created already, you can generate one using this command :

rails g devise:controllers users -c=sessions

(Assuming the model name is user)

The -c flag means the controller to generate, as we only need to generate SessionsController here. (You can read more about the devise generator command here)

This will generate a controller in app/controllers/users/sessions_controller.rb , and this will override the default Deviseā€™s SessionsController.

Next, we are going to override the ā€œcreateā€ method (the method that will be called when user sign in successfully) inside app/controllers/users/sessions_controller.rb :

class Users::SessionsController < Devise::SessionsController
  skip_before_action :check_concurrent_session

  # POST /resource/sign_in
  def create
    super
    # after the user signs in successfully, set the current login token
    set_login_token
  end

  private
  def set_login_token
    token = Devise.friendly_token
    session[:login_token] = token
    current_user.current_login_token = token
    current_user.save
  end
end

We use Devise.friendly_token to generate a random string for the token, then store it into the current session (session[:login_token]) and also save to the current userā€™s current_login_token column.

The skip_before_action :check_concurrent_session will be explained later, put it there for now.

Next, we will edit the application_controller.rb file (or whichever controller where most of the controllers that require authorization are inherited from).

class ApplicationController < ActionController::Base
  # perform the check before each controller action are executed
  before_action :check_concurrent_session

  private

  def check_concurrent_session
    if already_logged_in?
      # sign out the previously logged in user, only left the newly login user
      sign_out_and_redirect(current_user)
    end
  end

  def already_logged_in?
    # if current user is logged in, but the user login token in session 
    # doesn't match the login token in database,
    # which means that there is a new login for this user
    current_user && !(session[:login_token] == current_user.current_login_token)
  end
end

As we have wrote before_action :check_concurrent_session in the application_controller.rb , all controllers inherited from this will run the check_concurrent_session method before the controller action method are executed, this will check if there is any new login session initiated for the same user.

We want to exempt this check when user is on the login part, ie. sessions#create , hence we put a skip_before_action :check_concurrent_session to skip this check for the Users::SessionController class earlier.

Hereā€™s a demo video of this code :

demo

Notice that after the second login attempt, the previously logged in user gets logged out after clicking a link (executing a controller action).