Coder Social home page Coder Social logo

adamcooke / authie Goto Github PK

View Code? Open in Web Editor NEW
232.0 7.0 20.0 241 KB

๐Ÿ‘ฎโ€โ™‚๏ธ Improve user session security in Ruby on Rails applications with database session storage

License: MIT License

Ruby 93.61% HTML 6.39%
rails authentication ruby session-cookie persistent-sessions

authie's Introduction

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 now validate
    • persist! is now persist
    • invalidate! is now invalidate
    • touch! is now touch
    • set_cookie! is now set_cookie and is now a private method and should not be called directly.
    • see_password! is now see_password
    • mark_as_two_factored! is now mark_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 or create_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 and touch_auth_session runs after every action. If you don't want to touch a session in a request you can either use skip_around_action :touch_auth_session or call skip_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).

authie's People

Contributors

adamcooke avatar deanpcmad avatar gavrhy avatar github-actions[bot] avatar glacials avatar jimeh avatar paulsturgess avatar petergoldstein avatar skylarmacdonald avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

authie's Issues

SQL instead of ActiveRecord

This is good, but it would be better to bypass ActiveRecord and use SQL directly. I've found the following, but I don't know how up-to-date or secure or reliable they are. Rails would benefit from an up-to-date fast cookie/sql session store with the features you have here. Also I would like to see more adoption to convince me it's secure before I bet the business on it.

http://apidock.com/rails/v3.2.8/ActiveRecord/SessionStore/SqlBypass included in Rails 3.2
http://archive.railsforum.com/viewtopic.php?id=42802
http://kovyrin.net/2008/02/06/fastsessions-rails-plugin-released/
https://code.google.com/p/rails-fast-sessions/
http://railsexpress.de/blog/articles/2005/12/19/roll-your-own-sql-session-store/
https://gist.github.com/robertsosinski/3869435
https://github.com/nateware/sql_session_store
https://github.com/skaes/sql_session_store

Migration Error

If I try to run the migrations task, I get the following error (Rails 6.0.3.4, Ruby 2.7.2)

rake aborted!
NoMethodError: undefined method `helper_method' for ActionController::API:Class
/home/mkrs/pdms/config/environment.rb:5:in `<main>'
Tasks: TOP => railties:install:migrations => db:load_config => environment

EDIT

The problem seems to occur in this piece of code:

module Authie
  module ControllerExtension

    def self.included(base)
      base.helper_method :logged_in?, :current_user, :auth_session
      before_action_method = base.respond_to?(:before_action) ? :before_action : :before_filter
      base.public_send(before_action_method, :set_browser_id, :touch_auth_session)
    end

It happens the second time this function is called, when base is equal to ActionController::API
Seems to be an incompatibility issue with some inheritance.
Can I fix this on my side or has it to be fixed on the gem side?

Rails 6 + Zeitwerk name error

After upgrading to rails 6 and enabling the new autoloader, it seems rails is no longer able to find authie session properly throwing this error: uninitialized constant Authie::Session (NameError). The error is specifically being thrown because of this line rescue_from Authie::Session::ValidityError, with: :auth_session_error. If I turn back on the classic autoloader then no errors are thrown.

Let me know if you need anymore info from me about this.

What to do about default Rails session management?

I notice this gem doesn't act as a drop in replacement, i.e. ActiveRecord Session Store alternative. What do we do with existing Rails session management? For instance, the default Rails project will have:

# config/initialisers/ session_store.rb
Rails.application.config.session_store :cookie_store, key: '_my_app_session'

Would you remove ActionDispatch::Session::CookieStore Rack middleware?

Nice work btw.

User impersonation not returning to parent session

I have implemented the "user impersonation" feature as described in the readme and while the first step (impersonating as a user) works great, returning to the parent session does not.

After calling auth_session.revert_to_parent! i am completely logged out instead.

When logging in in a second browser window (inkognito) and looking at all sessions for the admin user, I can see that the session from which the impersonation is started is marked inactive once the impersonation starts and also marked active again when it ends, but in the browser where i end the impersonation i have no active session anymore.

Hereโ€™s the code (in a controller), which is pretty much exactly as it is in the readme:

def impersonate_user
  auth_session.impersonate!(User.find(params[:user_id]))
  redirect_to root_path
end

def revert_impersonation
  auth_session.revert_to_parent!
  redirect_to root_path
end

I hope that makes sense. Is there anything Iโ€™m missing?

Update: I can get it to work by grabbing the user off the parent session, destroying the parent session, and then starting a new session for the user. Seems a bit unnecessary though, Iโ€™d prefer to just properly activate the parent again.

def revert_impersonation
  parent_session = auth_session.revert_to_parent!
  parent_session.destroy!
  create_auth_session(parent_session.user)

  redirect_to root_path
end

Truncate Authie::Session#user_agent before it's persisted: "Data too long for column 'user_agent'"

Hi @adamcooke. I think d315875 is incomplete. I use version 1.2.4 of this gem on a project at work (thanks so much for your work on this) and required this fix after hitting the same issue that you undoubtedly did but I'm not quite ready to upgrade to the next major version.

The before_create above, where it assigns AuthieSession#user_agent in a controller context is still able to throw the same error.

Locally, I've implemented a fix for this by overriding AuthieSession#user_agent= like so:

module Authie
  class Session < ActiveRecord::Base

    def user_agent=(value)
      super value[0,255]
    rescue
      super
    end

  end
end

This seems to take care of the issue and allows me to save Authie::Session instances again.

I couldn't override the before_create as it's implemented with an anonymous block and wasn't sure I could guarantee the callback order if I'd added another before_create. before_create is the last callback in the stack before a record is persisted so I couldn't add another (different) one.

Just wanted to bring this to your attention. Hope this is helpful.

Upgrade from version 2 to 3 instructions

Hi @adamcooke , thanks for this great gem! ๐Ÿ™‚ ๐Ÿ’ฏ

In an existing project I would like to upgrade from authie version 2 to 3.

Can you provide some advices/instructions on how to do so?
The errors I get have to do with undefined method token_hash=' `. Is the solution to simply add a migration?

Thanks for your efforts!

Typo in ReadMe

Thanks for writing this gem.

Under Sudo functions I think the configuration setting
Authie.config.sudo_timeout = 30.minutes
should be
Authie.config.sudo_session_timeout = 30.minutes

Using Authie in route middleware

Hi there,

I've got a question regarding to using the current_user in a route constraint of Rails. I've got a route constraint in which I check the role of the authenticated user (if any) and adjust the root_path according to their role.

The code looks something like this;

class UserTypeConstraint
  def initialize(user_type)
    @user_type = user_type
  end

  def matches?(request)
    controller = Authie::RackController.new(request.env)
    current_user = controller.current_user
    current_user && current_user.send("#{@user_type}?".to_sym)
  end
end

But it seems the Authie::RackController can't be loaded. Any ideas why this happens, whether it's intended or am I just doing something wrong?

Cheers

v3.3.0 release?

This commit, which changed auto-loading behaviour, bumped the version to 3.3.0 but that hasnโ€™t been released to RubyGems yet (the latest is 3.2.0).

I assume that that change also closes #17, as it completely removes the line in question. That would be great because I could install authie regularly instead of pointing to master when upgrading to Rails 6.

So:

  1. Release 3.3.0 to RubyGems
  2. Close #17 (?)

Thank you!

Toggle Authie in certain controllers

We have some controllers for a public api that we would like to turn off authie for. Is this possible to do this cleanly? I tried doing a skip_before_action with set_browser_id and touch_auth_session but it still runs some of the code. We would like to be able to disable it completely for all but 1 or 2 of the endpoints as they don't need any kind of session checking (some of the endpoints still rely on cookie authentication).

Error when using UUID

Hi, how to make it work with uuid?
because its error "Operand type clash: int is incompatible with uniqueidentifier "

Use bigint in migrations

Currently id type columns are created as integers, Since Rails 5.1 the default has been bigint, so it would make sense to update the Authie migrations to use bigint as well

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.