Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implement single session module #218

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/generators/sorcery/templates/migration/single_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class SorcerySingleSession < <%= migration_class_name %>
def change
add_column :<%= model_class_name.tableize %>, :session_token, :string, default: nil
end
end
2 changes: 2 additions & 0 deletions lib/sorcery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Submodules
require 'sorcery/model/submodules/brute_force_protection'
require 'sorcery/model/submodules/external'
require 'sorcery/model/submodules/magic_login'
require 'sorcery/model/submodules/single_session'
end
end

Expand All @@ -33,6 +34,7 @@ module Submodules
require 'sorcery/controller/submodules/http_basic_auth'
require 'sorcery/controller/submodules/activity_logging'
require 'sorcery/controller/submodules/external'
require 'sorcery/controller/submodules/single_session'
end
end

Expand Down
47 changes: 47 additions & 0 deletions lib/sorcery/controller/submodules/single_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Sorcery
module Controller
module Submodules
module SingleSession
def self.included(base)
base.send(:include, InstanceMethods)

Config.module_eval do
class << self
attr_accessor :verify_session_token_enabled
def merge_remember_me_defaults!
@defaults.merge!(:@verify_session_token_enabled => true)
end
end
merge_remember_me_defaults!
end

unless Config.after_login.include?(:set_session_token)
Config.after_login << :set_session_token
end

base.prepend_before_action :verify_session_token, if: :logged_in?
end

module InstanceMethods
# Checks if session token matches users
# To be used as a before_action
def verify_session_token
return unless Config.verify_session_token_enabled
return if sorcery_session_token_valid?

reset_sorcery_session
remove_instance_variable :@current_user if defined? @current_user
end

def sorcery_session_token_valid?
session[:token] == current_user.session_token
end

def set_session_token(user, _credentials = nil)
session[:token] = user.regenerate_session_token
end
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/sorcery/model/submodules/single_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Sorcery
module Model
module Submodules
# This submodule adds the ability to set unique session token per user.
# It helps enforce single session per user.
# This is the model part of the submodule, which provides configuration options.
module SingleSession
def self.included(base)
base.sorcery_config.class_eval do
# Unique session token attribute name
attr_accessor :session_token_attribute_name
end

base.sorcery_config.instance_eval do
@defaults.merge!(:@session_token_attribute_name => :session_token)
reset!
end

base.sorcery_config.after_config << :define_session_token_fields

base.extend(ClassMethods)
base.send(:include, InstanceMethods)
end

module ClassMethods

protected

def define_session_token_fields
class_eval do
sorcery_adapter.define_field sorcery_config.session_token_attribute_name, String
end
end
end

module InstanceMethods
def regenerate_session_token
token = TemporaryToken.generate_random_token
sorcery_adapter.update_attributes({ sorcery_config.session_token_attribute_name => token })

token
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/sorcery/test_helpers/internal/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Rails
register_last_activity_time_to_db
deny_banned_user
validate_session
verify_session_token
].freeze

def sorcery_reload!(submodules = [], options = {})
Expand Down
15 changes: 15 additions & 0 deletions spec/active_record/user_single_session_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'spec_helper'
require 'shared_examples/user_single_session_shared_examples'

describe User, 'with single_session submodule', active_record: true do
before(:all) do
MigrationHelper.migrate("#{Rails.root}/db/migrate/single_session")
User.reset_column_information
end

after(:all) do
MigrationHelper.rollback("#{Rails.root}/db/migrate/single_session")
end

it_behaves_like 'rails_single_session_model'
end
48 changes: 48 additions & 0 deletions spec/controllers/controller_single_session_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'spec_helper'

describe SorceryController, type: :controller do
let!(:user) { double('user', id: 42) }

context 'with session token features' do
before(:all) do
sorcery_reload!([:single_session])
end

after(:all) do
sorcery_controller_property_set(:verify_session_token_enabled, false)
end

before(:each) do
allow(user).to receive(:session_token) { 'valid-session-token' }
allow(user).to receive(:regenerate_session_token) { 'valid-session-token' }

allow(user).to receive(:email)
allow(user).to receive_message_chain(:sorcery_config, :username_attribute_names, :first) { :email }
end

it 'does not reset session if token is valid' do
login_user user
session[:token] = 'valid-session-token'

get :test_should_be_logged_in

expect(session[:user_id]).not_to be_nil
expect(response).to be_successful
end

it 'does reset session if token is invalid' do
login_user user
session[:token] = 'invalid-session-token'

get :test_should_be_logged_in

expect(session[:user_id]).to be_nil
expect(response).not_to be_successful
end

it 'regenerates token on login' do
expect(user).to receive(:regenerate_session_token)
login_user user
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddSessionTokenToUsers < ActiveRecord::CompatibleLegacyMigration.migration_class
def change
add_column :users, :session_token, :string, default: nil
end
end
42 changes: 42 additions & 0 deletions spec/shared_examples/user_single_session_shared_examples.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
shared_examples_for 'rails_single_session_model' do
# ----------------- PLUGIN CONFIGURATION -----------------------
let(:user) { create_new_user }

describe 'loaded plugin configuration' do
before(:all) do
sorcery_reload!([:single_session])
end

after(:each) do
User.sorcery_config.reset!
end

context 'API' do
specify { expect(user).to respond_to :session_token }

specify { expect(user).to respond_to :regenerate_session_token }
end

it "allows configuration option 'session_token_attribute_name'" do
sorcery_model_property_set(:session_token_attribute_name, :random_token)

expect(User.sorcery_config.session_token_attribute_name).to eq :random_token
end
end

describe 'when activated with sorcery' do
before(:all) do
sorcery_reload!([:single_session])
end

describe '#regenerate_session_token' do
it 'generates and updates user record with new random session token' do
expect(user.session_token).to be_nil

token = user.regenerate_session_token

expect(user.session_token).to eq token
end
end
end
end