Skip to content

Instantly share code, notes, and snippets.

@KieranP
Last active October 8, 2020 16:11
Show Gist options
  • Save KieranP/9044432 to your computer and use it in GitHub Desktop.
Save KieranP/9044432 to your computer and use it in GitHub Desktop.
A Ruby on Rails implementation of a Database and Cookie hybrid session storage for Devise that supports session revocation and storage of session ip and user agent for security (similar to Github)
# A user session class, a simplified mix of the following code samples:
# * https://github.com/blog/1661-modeling-your-app-s-user-session
# * http://www.jonathanleighton.com/articles/2013/revocable-sessions-with-devise/
class Session < ActiveRecord::Base
# Uncomment if you use Hobo Fields, else add these yourself
# fields do
# session_id :string, :index => true, :unique => true
# accessed_at :datetime
# user_ip :string
# user_agent :string
# timestamps
# end
belongs_to :user
validates_presence_of :user, :session_id
def self.exists?(user, session_id)
self.where(:user => user, :session_id => session_id).exists?
end
def self.fetch(user, session_id)
if user
user.sessions.find_by_session_id(session_id)
else
Session.find_by_session_id(session_id)
end
end
def self.activate(user)
self.purge_old(user) # Clear old sessions before creating one
user.sessions.create!(:session_id => SecureRandom.hex)
end
def self.deactivate(user, session_id)
self.fetch(user, session_id).try(:delete)
end
def self.purge_old(user)
user.sessions.where { accessed_at < 2.weeks.ago }.destroy_all
end
def accessed!(request)
self.accessed_at = Time.zone.now
self.user_ip = request.remote_ip
self.user_agent = request.user_agent
self.save!
end
end
class User < ActiveRecord::Base
has_many :sessions, :dependent => :destroy
end
Warden::Manager.after_set_user(:except => :fetch) do |user, warden, opts|
unless Rails.env.test?
Session.deactivate(user, warden.cookies.signed["_session_id"])
warden.cookies.signed["_session_id"] = {
:value => Session.activate(user).session_id,
:expires => 1.year.from_now,
:httponly => true
}
end
end
Warden::Manager.after_fetch do |user, warden, opts|
unless Rails.env.test?
if Session.exists?(user, warden.cookies.signed["_session_id"])
warden.cookies.signed["_session_id"] = {
:value => warden.cookies.signed["_session_id"],
:expires => 1.year.from_now,
:httponly => true
}
else
warden.logout
throw(:warden, :message => :unauthenticated)
end
end
end
Warden::Manager.before_logout do |user, warden, opts|
unless Rails.env.test?
Session.deactivate(user, warden.cookies.signed["_session_id"])
warden.cookies.delete("_session_id")
end
end
class ApplicationController < ActionController::Base
before_filter :authenticate_user!, :update_current_session
def current_session
return unless current_user
@current_session ||= current_user.sessions.find_by_session_id(cookies.signed[:_session_id])
end
def update_current_session
return unless current_session
current_session.accessed!(request)
end
end
# We use a hybrid database/cookie based session storage
[APP_CONSTANT]::Application.config.session_store :cookie_store, key: APP_CONFIG[:session_key]
@abcnever
Copy link

did you figure out how to add the Warden::Manage callbacks without checking for test environment? I'd like to write rspec test for my controller that involves revoking user's other login sessions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment