Authie
This is a Rails library which provides applications with a database-backed user sessions. This ensures that user sessions can be invalidated from the server and users activity can be easily tracked.
The "traditional" way of simply setting a user ID in your session is insecure and unwise. If you simply do something like the example below, it means that anyone with access to the session cookie can login as the user whenever and wherever they wish.
To clarify: while by default Rails session cookies are encrypted, there is nothing to allow them to be invalidated if someone were to "steal" an encrypted cookie from an authenticated user. This could be stolen using a MITM attack or simply by stealing it directly from their browser when they're off getting a coffee.
if user = User.authenticate(params[:username], params[:password])
# Don't do this...
session[:user_id] = user.id
redirect_to root_path, :notice => "Logged in successfully!"
end
The design goals behind Authie are:
- Any session can be invalidated instantly from the server without needing to make changes to remote cookies.
- We can see who is logged in to our application at any point in time.
- Sessions should automatically expire after a certain period of inactivity.
- Sessions can be either permanent or temporary.
Installation
As usual, just pop this in your Gemfile:
gem 'authie', '~> 4.0'
You will then need add the database tables Authie needs to your database. You should copy Authie's migrations and then migrate.
rake authie:install:migrations
rake db:migrate
Usage
Authie is just a session manager and doesn't provide any functionality for your authentication or User models. Your User
model should implement any methods needed to authenticate a username & password.
Creating a new session
When a user has been authenticated, you can simply set current_user
to the user
you wish to login. You may have a method like this in a controller.
class SessionsController < ApplicationController
def create
if user = User.authenticate(params[:username], params[:password])
create_auth_session(user)
redirect_to root_path
else
flash.now[:alert] = "Username/password was invalid"
end
end
end
Checking whether user's are logged in
On any subsequent request, you should make sure that your user is logged in.
You may wish to implement a login_required
controller method which is called
before every action in your application.
class ApplicationController < ActionController::Base
before_action :login_required
private
def login_required
return if logged_in?
redirect_to login_path, :alert => "You must login to view this resource"
end
end
Accessing the current user (and session)
There are a few controller methods which you can call which will return information about the current session:
-
current_user
- returns the currently logged in user -
auth_session
- returns the current auth session -
logged_in?
- returns a true if there's a session or false if no user is logged in
Catching session errors
If there is an issue with an auth session, an error will be raised which you need to catch within your application. The errors which will be raised are:
-
Authie::Session::InactiveSession
- is raised when a session has been de-activated. -
Authie::Session::ExpiredSession
- is raised when a session expires. -
Authie::Session::BrowserMismatch
- is raised when the browser ID provided does not match the browser ID associated with the session token provided. -
Authie::Session::HostMismatch
- is raised when the session is used on a hostname that does not match that which created the session
The easiest way to rescue these to use a rescue_from
. For example:
class ApplicationController < ActionController::Base
rescue_from Authie::Session::ValidityError, :with => :auth_session_error
private
def auth_session_error
redirect_to login_path, :alert => "Your session is no longer valid. Please login again to continue..."
end
end
Logging out
In order to invalidate a session you can simply invalidate it.
def logout
auth_session.invalidate
redirect_to login_path, :notice => "Logged out successfully."
end
Default session length
By default, a session will last for however long it is being actively used in browser. If the user stops using your application, the session will last for 12 hours before becoming invalid. You can change this:
Authie.config.session_inactivity_timeout = 2.hours
This does not apply if the session is marked as persistent. See below.
Persisting sessions
In some cases, you may wish users to have a permanent sessions. In this case, you should ask users after they have logged in if they wish to "persist" their session across browser restarts. If they do wish to do this, just do something like this:
def persist_session
auth_session.persist
redirect_to root_path, :notice => "You will now be remembered!"
end
By default, persistent sessions will last for 2 months before requring the user logs in again. You can increase (or decrease) this if needed:
Authie.config.persistent_session_length = 12.months
Accessing all user sessions
If you want to provide users with a list of their sessions, you can access all
active sessions for a user. The best way to do this will be to add a has_many
association to your User model.
class User < ActiveRecord::Base
has_many :sessions, :class_name => 'Authie::SessionModel', :as => :user, :dependent => :destroy
end
Storing additional data in the user session
If you need to store additional information in your database-backed database session, then you can use the following methods to achieve this:
auth_session.set :two_factor_seen_at, Time.now
auth_session.get :two_factor_seen_at
Invalidating all but current session
You may wish to allow users to easily invalidate all sessions which aren't their
current one. Some applications invalidate old sessions whenever a user changes
their password. The invalidate_others!
method can be called on any
Authie::Session
object and will invalidate all sessions which aren't itself.
def change_password
@user.change_password(params[:new_password])
auth_session.invalidate_others!
end
Sudo functions
In some applications, you may want to require that the user has recently provided their password to you before executing certain sensitive actions. Authie provides some methods which can help you keep track of when a user last provided their password in a session and whether you need to prompt them before continuing.
# When the user logs into your application, run the see_password method to note
# that we have just seen their password.
def login
if user = User.authenticate(params[:username], params[:password])
create_auth_session(user, see_password: true)
redirect_to root_path
end
end
# Before executing any dangerous actions, check to see whether the password has
# recently been seen.
def change_password
if auth_session.recently_seen_password?
# Allow the user to change their password as normal.
else
# Redirect the user a page which allows them to re-enter their password.
# The method here should verify the password is correct and call the
# see_password method as above. Once verified, you can return them back to
# this page.
redirect_to reauth_path(:return_to => request.fullpath)
end
end
By default, a password will be said to have been recently seen if it has been seen in the last 10 minutes. You can change this configuration if needed:
Authie.config.sudo_session_timeout = 30.minutes
Working with two factor authentication
Authie provides a couple of methods to help you determine when two factor authentication is required for a request. Whenever a user logs in and has enabled two factor authentication, you can mark sessions as being permitted.
You can add the following to your application controller and ensure that it runs on every request to your application.
class ApplicationController < ActionController::Base
before_action :check_two_factor_auth
def check_two_factor_auth
if logged_in? && current_user.has_two_factor_auth? && !auth_session.two_factored?
# If the user has two factor auth enabled, and we haven't already checked it
# in this auth session, redirect the user to an action which prompts the user
# to do their two factor auth check.
redirect_to two_factor_auth_path
end
end
end
Then, on your two factor auth action, you need to ensure that you mark the auth session as being verified with two factor auth.
class LoginController < ApplicationController
skip_before_action :check_two_factor_auth
def two_factor_auth
if user.verify_two_factor_token(params[:token])
auth_session.mark_as_two_factored
redirect_to root_path, :notice => "Logged in successfully!"
end
end
end
Storing IP address countries
Authie has support for storing the country that an IP address is located in whenever they are saved to the database. To use this, you need to specify a backend to use in the Authie configuration. The backend should respond to #call(ip_address)
.
Authie.config.lookup_ip_country_backend = proc do |ip_address|
SomeService.lookup_country_from_ip(ip_address)
end
Instrumentation/Notification
Authie will publish events to the ActiveSupport::Notification instrumentation system. The following events are published with the given attributes.
-
set_browser_id.authie
- when a new browser ID is set for a user. Provides:browser_id
and:controller
arguments. -
cleanup.authie
- when session cleanup is run. Provides no arguments. -
touch.authie
- when a session is touched. Provides:session
argument. -
see_password.authie
- when a session sees a password. Provides:session
argument. -
mark_as_two_factor.authie
- when a session has two factor credentials provided. Provides:session
argument. -
session_start.authie
- when a session is started. Provides:session
argument. -
session_invalidate.authie
- when a session is intentionally invalidated. Provides:session
argument with session model instance. -
browser_id_mismatch_error.authie
- when a session is validated when the browser ID does not match. Provides:session
argument. -
invalid_session_error.authie
- when a session is validated when invalid. Provides:session
argument. -
expired_session_error.authie
- when a session is validated when expired. Provides:session
argument. -
inactive_session_error.authie
- when a session is validated when inactive. Provides:session
argument. -
host_mismatch_error.authie
- when a session is validated and the host does not match. Provides:session
argument.
Differences for Authie 4.0
Authie 4.0 introduces a number of changes to the library which are worth noting when upgrading from any version less than 4.
- Authie 4.0 removes the impersonation features which may make a re-appearance in a futre version.
- All previous callback/events have been replaced with standard ActiveSupport instrumentation notifications.
-
Authie::SessionModel
has been introduced to represent the instance of the underlying database record. - Various methods on Authie::Session (more commonly known as
auth_session
) have been renamed as follows.-
check_security!
is nowvalidate
-
persist!
is nowpersist
-
invalidate!
is nowinvalidate
-
touch!
is nowtouch
-
set_cookie!
is nowset_cookie
and is now a private method and should not be called directly. -
see_password!
is nowsee_password
-
mark_as_two_factored!
is nowmark_as_two_factored
-
- A new
Authie::Session#reset_token
has been added which will generate a new token for a session, save it and update the cookie. - When starting a session using
Authie::Session.start
orcreate_auth_session
you can provide the following additional options:-
persistent: true
to mark the session as persistent (i.e. give it an expiry time) -
see_password: true
to set the password seen timestamp at the same time as creation
-
- If the
extend_session_expiry_on_touch
config option is set to true (default is false), the expiry time for a persistent session will be extended whenver a session is touched. - When making a request, the session will be touched after the action rather than before. Previously, the
touch_auth_session
method was added before every action and it both validated the session and touched it. Now, there are two separate methods -validate_auth_session
which is run before every action andtouch_auth_session
runs after every action. If you don't want to touch a session in a request you can either useskip_around_action :touch_auth_session
or callskip_touch_auth_session!
anywhere in the action. - A new config option called
session_token_length
is available which allows you to change the length of the random token used for sessions (default 64).