Question

Question: What's a good way to "lock" a Rails web application so a user must enter credentials to unlock it (but such that the user is still logged into the app and simply can't use the app without unlocking it)?

What I'm doing

I'm building a task management app that behaves like a kiosk to be used by multiple users. Users can log in to the app using their email address and password (full credentials), but once they're in, they can "swap" users by selecting their name from a list and entering a four-digit PIN (easy credentials).

Why have two types of credentials?

This is a multi-tenant app, and users use "full credentials" to simply get into the app and choose a tenant. Once they're in, they will frequently swap out as new employees come on shift and other employees leave because multiple employees will share a single work station.

Rather than forcing users to repeatedly enter their "full" credentials (by doing a full logout/login), I've created a name/PIN set of credentials they can use to easily swap. Once they're logged in with full credentials and have chosen their tenant, the app provides an autocomplete list of user's names, so it's very easy to select one's name from the list, enter the PIN and swap users.

Note that this "swapping" functionality is already built and working.

Why do I need to lock the app?

When a user is logged in to the app and a tenant, they may need to leave the app to go do other work. Without a lock screen, they only have two choices: fully log out so that the next user has to enter "full" credentials to use the app or just leave their account logged in for anyone to mess with.

I would like to allow them to click a button to "lock" the app when they walk away so that users can leave the app "logged in" without allowing other users to access others' accounts. Once they click "lock" all they can do is enter a name/PIN to unlock it, or click "sign out" to fully log out.

Here's what a typical use-case would look like:

  • Frank logs into the app using his email address and password.
  • He completes some tasks.
  • Jane wants to use the app, so she "swaps in" by choosing her name from a list and entering her four-digit PIN.
  • Now Jane is the logged-in user, meaning she can complete tasks, edit her profile, and other activities.
  • Jane completes some tasks and needs to go do something else in the store, so she clicks the "Lock" button on the screen.
  • The app is now locked and requires authentication to use (name and PIN) or the user can click "sign out" to fully go out of the app. The screen simply says something like "The app is locked. Enter your name and PIN to unlock it."

The options as I understand them

It seems like there are two ways to go here:

  1. JavaScript of some kind
  2. Some sort of controller logic using session variables or cookies

JavaScript makes me nervous because it's pretty hackable. I want to make sure the app is actually secure.

So I've been messing with ways of using session variables to do this. Here is what I've been trying:

  • User is authenticated to the app for a particular tenant
  • User clicks the "Lock" button
  • A "lock_ui" action is called in the Sessions controller
  • That action sets a few session variables (session[:locked], session[:locked_by], session[:locked_at])
  • The app then always redirects to Sessions#locked, which displays the "This is locked. Enter name and PIN to unlock" form unless it's unlocked or the user clicks "sign out".
  • Once a user enters a valid name/PIN, those session variables are deleted and the app functions normally.

Any suggestions on how I might do this so that it's secure?

Things I'm hung up on:

  • Should I be using some sort of application controller filter (before or around) to check to see if the app is locked? What does the application controller do if the app is locked? (Does it call some other action, render a the "locked" page directly, or something else?)
  • Or is this something that should be handled in the view layer? For example, I could update my application layout so it has an "if locked_ui render the 'locked' screen, else yield".

Some notes on how I'm currently set up:

  • This is a Rails 4.1.0 app using Ruby 1.9.3 hosted on heroku.
  • I'm using CanCan for authorization
  • I'm using multi-tenancy with scopes (and I'm not using sub-domains)
  • Each user has only one account that may have access to multiple tenants
  • I've seen JQuery BlockUI, but it seems more like a vanity thing than a functional security device
Was it helpful?

Solution

Here's what I ended up building.

To answer my own questions:

Should I be using some sort of application controller filter (before or around) to check to see if the app is locked? I am using a before_filter in my Application Controller to check whether the UI is locked. If it's locked, I redirect to the "locked" screen. I then added a skip_before_filter to the relevant actions in my Sessions Controller (essentially ignoring that before_filter when the user is navigating to the "locked" screen and related lock/unlock actions in the Sessions controller.

Or is this something that should be handled in the view layer? In the view layer, I mostly just needed to create the actual "locked" screen (where a user enters credentials to unlock the app), and I make sure to hide navigation elements while the UI is locked (so the user isn't confused as to why they click "Edit Profile" and don't leave the lock screen).

I'm using cookies to record the current state of the UI (locked or not). Here are some code snippets:

application_controller.rb

before_filter :confirm_unlocked_ui

private

def confirm_unlocked_ui
  if signed_in? && locked_ui?
    redirect_to locked_path
  end
end

sessions_helper.rb

def locked_ui?
  session[:locked] == '1'
end

def lock_ui
  session[:locked] = '1'
  session[:locked_by] = current_user.id
  session[:locked_at] = Time.zone.now
end

def unlock_ui
  session.delete(:locked)
  session.delete(:locked_by)
  session.delete(:locked_at)
end

sessions_controller.rb

def lock
  session[:return_to] = params[:return_to] if params[:return_to]
  lock_ui

  redirect_to locked_path
end

def locked
  #essentially just a view
end

def unlock
  user = User.find_by_id(params[:session][:unlock_user_id]) 
  if user && user.authenticate_with_pin(params[:session][:pin])
    cookies[:auth_token] = user.auth_token
    unlock_ui
    redirect_back_or root_path
  else      
    flash.now[:error] = "Invalid PIN."
    render 'locked'
  end
end

def destroy
  sign_out
  unlock_ui
  redirect_to root_path
end

I really don't know how "good" this solution is, and one reason I'm posting it here is to see if others come up with better ideas or see any issues lurking with how I've done this.

Some more subtle things you might want to think about if you're trying this:

  • I'm using CanCan for authorization, and I've left that out here. If you have role-based authorization or something like that, be sure to check that this works for each role.
  • Notice that I allow users to 'sign out' if they don't want to 'unlock'. I do this using the 'destroy' action my sessions controller and I make sure to 'unlock_ui' before I redirect them after signing out. If I didn't do this, then the app would still be "locked" the next time they log in from that browser.
  • locked_path is an alias for Sessions#locked (basically just a view/form) that responds to a get request.
  • Sessions#Lock also responds to get requests.
  • Sessions#Unlock is what is called when the form is submitted from the "locked" view.
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top