Summary

At work we started enforcing users to be logged out after 30 minutes to improve security.

The way we were doing it lead to an issue with users being unable to complete forms due to getting logged out while going on lunch.

I needed a way of giving a user an easy way of logging back in after being logged out.

So I made an easy way for users to sign back in after getting automatically signed out using two gems Devise Gem, Auto Session Timeout and some custom code.


Login Modal


Devise Gem

I used devise as the back bone of my authentication.

Devise Gem.

You should be able follow those instructions to get devise setup and follow on for there.

If you didn’t use devise you will have changes to make in your routes and where certain methods are placed.

Auto Session Timeout Gem

I hoped to find a gem that would solve all my issues and I found an interesting Gem Auto Session Timeout but it only gave me some of the features I needed.

When setup it’ll allow you to automatically redirect a user to the login in page or other route after a user hasn’t sent an action to the server in 30 minutes.

Auto Session Timeout Setup

The main feature I wanted to be able to show a user a login modal automatically and not have to leave their current page or progress so I made some changes.

Follow the instructions from Auto Session Timeout for the devise setup.

Instead of the timeout route make a login_from_modal route. This will (You might have guessed it) allow us to login from the modal.

config\routes.rb

devise_scope :user do
  post 'login_from_modal', to: "users/sessions#login_from_modal"
  get "active", to: "users/sessions#active"
end

Another small change I made using Devise.timeout_in to set the auto_session_timeout.

app\controllers\application_controller.rb

class ApplicationController < ActionController::Base

before_action :authenticate_user!
auto_session_timeout Devise.timeout_in

end

The next change we want is the ability to show a modal instead of redirecting to the login page. To make these changes I pulled out the code they had in their helper method (auto_session_timeout_helper.rb) and made one change. I just wanted to be able to show a modal when the user was inactive for 30 minutes. Every 60 seconds the server is pinged to check if the user has completed any actions in the last 30 minutes. If they haven’t they will be logged out.

app\views\layouts\application.html.erb

To change the redirect to a modal show you’ll want to replace

<%= auto_session_timeout_js %>

with

<script>
function periodical_query() {
  var request = new XMLHttpRequest();
  request.onload = function (event) {
    var status = event.target.status;
    var response = event.target.response;
    if (
      status === 200 &&
      (response === false || response === "false" || response === null)
    ) {
      document.getElementById("login-modal").style.display = "block"; // This line was added to display the modal
    }
  };
  request.open("GET", "/active", true);
  request.responseType = "json";
  request.send();
  setTimeout(periodical_query, 60 * 1000); // Pings server every 60 seconds
}
setTimeout(periodical_query, 60 * 1000);
</script>

On the same page I then rendered the login modal.

<%= render 'layouts/login_modal'%>

Login Modal

I added in some code from w3schools just to build a quick modal.

app\views\layouts\application.html.erb

<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css" />

app\views\layouts_login_modal.html.erb

For my modal I created a form with two fields. One for the users email and another for their password. The user is only shown one field for their password. ( As they will want to log back into the same account ) I’m using the devise method current_user.email to get the users email who is currently logged in and setting that to a hidden field.

<div id="login-modal" class="w3-modal">
  <div class="w3-modal-content">
    <div class="w3-container">
      <div class="col-10 mx-auto">
        <div class="col-12">
          <p class="text-center my-3" style="font-size: 1rem;">
            You have been logged out due to inactivity. Please enter your
            password to log in.
          </p>
        </div>
        <%### Flash %>
        <div
          id="login-modal-flash-message"
          class="pt-3 mb-2"
          style="display: none;"
        >
          <p>Incorrect Password</p>
        </div>
        <%### Hidden Email Address %> <%= hidden_field_tag "login-modal-email",
        "#{current_user&.email}" %> <%### Password %>
        <div class="col-12 form-group">
          <label for="password" class="form-label">Password</label>
          <%= password_field_tag "password", "", class: "form-control",
          id:"login-modal-password", placeholder: "Password", required: true %>
        </div>
        <%### Submit Button %>
        <div class="actions text-center m-t-20 m-b-20 col-12">
          <%= submit_tag "Log In", id:"login-modal-submit-button", class:"btn
          btn-primary font-16 w-100", onclick: "login_modal_submit(this);" %>
        </div>
      </div>
    </div>
  </div>
</div>


Login Modal


Login Modal - Javascript

Pressing the submit button causes a javascript function to be called to handle the event. The users password and email are grabbed from the forum and sent to the sessions controller. On success the modal is hidden. On failure message is shown that the password is incorrect.

var login_modal_submit = function (element) {
  var password = document.getElementById("login-modal-password").value;
  var email = document.getElementById("login-modal-email").value;
  var request = new XMLHttpRequest();
  request.onload = function (event) {
    var status = event.target.status;
    if (status === 200) {
      document.getElementById("login-modal").style.display = "none";
    } else {
      document.getElementById("login-modal-flash-message").style.display =
        "block";
      setTimeout(() => {
        var login_message = document.getElementById(
          "login-modal-flash-message"
        );
        login_message.style.display = "none";
      }, 1000);
    }
  };

  request.open("POST", "<%= create_from_ajax_path %>", true);
  request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  request.send("email=" + email + "&password=" + password + "");
};

Login Modal - Controller

In my sessiosn controller I then added a login_from_modal method to handle checking if the user details passed were valid. If would then return a success or failure message.

app\controllers\users\sessions_controller.rb

class Users::SessionsController < Devise::SessionsController

  auto_session_timeout_actions

  ## Skip authentication as the user is logged out for these actions
  skip_before_action :authenticate_user!, only: [:login_from_modal], :raise => false
  skip_before_action :verify_authenticity_token, only: [:login_from_modal]

  # Checks if the user details are valid
  def login_from_modal
    resource = User.find_for_database_authentication(email: params[:email])
    return invalid_login_attempt unless resource
    if resource.valid_password?(params[:password])
      sign_in :user, resource
      render json: { message: "success" }, :status => 200
    else
      invalid_login_attempt
    end
  end

  def active
    render_session_status
  end

  private

    def invalid_login_attempt
      render json: { message: "Invalid Email or Password" }, :status => 401
    end

end

Hopefully at the end of all that you have a nice automatic modal to login with.

Road Ahead

Over the next week I plan to:

Look at adding Action Cable to my project to so that successfully logging in on one modal will close all login modals for that user.

Thanks for reading.