From 50c64530aac593a28a3842ff63826c64509f7a8f Mon Sep 17 00:00:00 2001 From: Mladen Ilic Date: Sat, 21 Dec 2019 22:58:44 +0100 Subject: [PATCH 1/4] Implement single session module --- lib/sorcery.rb | 2 + .../controller/submodules/single_session.rb | 47 +++++++++++++++++++ .../model/submodules/single_session.rb | 47 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 lib/sorcery/controller/submodules/single_session.rb create mode 100644 lib/sorcery/model/submodules/single_session.rb diff --git a/lib/sorcery.rb b/lib/sorcery.rb index 2a0af8cc..4f5d1ecb 100644 --- a/lib/sorcery.rb +++ b/lib/sorcery.rb @@ -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 @@ -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 diff --git a/lib/sorcery/controller/submodules/single_session.rb b/lib/sorcery/controller/submodules/single_session.rb new file mode 100644 index 00000000..72be215e --- /dev/null +++ b/lib/sorcery/controller/submodules/single_session.rb @@ -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.after_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 diff --git a/lib/sorcery/model/submodules/single_session.rb b/lib/sorcery/model/submodules/single_session.rb new file mode 100644 index 00000000..c8633784 --- /dev/null +++ b/lib/sorcery/model/submodules/single_session.rb @@ -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 From 7b2e361495554e78e029cfbf1da4fa96ec44457b Mon Sep 17 00:00:00 2001 From: Mladen Ilic Date: Sun, 22 Dec 2019 02:59:47 +0100 Subject: [PATCH 2/4] Add single session migration template --- lib/generators/sorcery/templates/migration/single_session.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 lib/generators/sorcery/templates/migration/single_session.rb diff --git a/lib/generators/sorcery/templates/migration/single_session.rb b/lib/generators/sorcery/templates/migration/single_session.rb new file mode 100644 index 00000000..2ec35861 --- /dev/null +++ b/lib/generators/sorcery/templates/migration/single_session.rb @@ -0,0 +1,5 @@ +class SorcerySingleSession < <%= migration_class_name %> + def change + add_column :<%= model_class_name.tableize %>, :session_token, :string, default: nil + end +end From 117466b15892092170b685347c1ebb0be03454df Mon Sep 17 00:00:00 2001 From: Mladen Ilic Date: Sun, 22 Dec 2019 03:00:12 +0100 Subject: [PATCH 3/4] Add single session module spec --- lib/sorcery/test_helpers/internal/rails.rb | 1 + .../active_record/user_single_session_spec.rb | 15 ++++++ .../controller_single_session_spec.rb | 48 +++++++++++++++++++ ...191221223622_add_session_token_to_users.rb | 5 ++ .../user_single_session_shared_examples.rb | 42 ++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 spec/active_record/user_single_session_spec.rb create mode 100644 spec/controllers/controller_single_session_spec.rb create mode 100644 spec/rails_app/db/migrate/single_session/20191221223622_add_session_token_to_users.rb create mode 100644 spec/shared_examples/user_single_session_shared_examples.rb diff --git a/lib/sorcery/test_helpers/internal/rails.rb b/lib/sorcery/test_helpers/internal/rails.rb index a9eb4365..0110411e 100644 --- a/lib/sorcery/test_helpers/internal/rails.rb +++ b/lib/sorcery/test_helpers/internal/rails.rb @@ -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 = {}) diff --git a/spec/active_record/user_single_session_spec.rb b/spec/active_record/user_single_session_spec.rb new file mode 100644 index 00000000..5aa5fae1 --- /dev/null +++ b/spec/active_record/user_single_session_spec.rb @@ -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 diff --git a/spec/controllers/controller_single_session_spec.rb b/spec/controllers/controller_single_session_spec.rb new file mode 100644 index 00000000..696d3336 --- /dev/null +++ b/spec/controllers/controller_single_session_spec.rb @@ -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).to be_successful + end + + it 'regenerates token on login' do + expect(user).to receive(:regenerate_session_token) + login_user user + end + end +end diff --git a/spec/rails_app/db/migrate/single_session/20191221223622_add_session_token_to_users.rb b/spec/rails_app/db/migrate/single_session/20191221223622_add_session_token_to_users.rb new file mode 100644 index 00000000..395616e7 --- /dev/null +++ b/spec/rails_app/db/migrate/single_session/20191221223622_add_session_token_to_users.rb @@ -0,0 +1,5 @@ +class AddSessionTokenToUsers < ActiveRecord::CompatibleLegacyMigration.migration_class + def change + add_column :users, :session_token, :string, default: nil + end +end diff --git a/spec/shared_examples/user_single_session_shared_examples.rb b/spec/shared_examples/user_single_session_shared_examples.rb new file mode 100644 index 00000000..5b763036 --- /dev/null +++ b/spec/shared_examples/user_single_session_shared_examples.rb @@ -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 From 229da8bb4b03aba2d221cb48bb9e2090ba0a2f39 Mon Sep 17 00:00:00 2001 From: Mladen Ilic Date: Sun, 22 Dec 2019 03:14:23 +0100 Subject: [PATCH 4/4] Verify session token before require_login is called --- lib/sorcery/controller/submodules/single_session.rb | 2 +- spec/controllers/controller_single_session_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sorcery/controller/submodules/single_session.rb b/lib/sorcery/controller/submodules/single_session.rb index 72be215e..681c5a0c 100644 --- a/lib/sorcery/controller/submodules/single_session.rb +++ b/lib/sorcery/controller/submodules/single_session.rb @@ -19,7 +19,7 @@ def merge_remember_me_defaults! Config.after_login << :set_session_token end - base.after_action :verify_session_token, if: :logged_in? + base.prepend_before_action :verify_session_token, if: :logged_in? end module InstanceMethods diff --git a/spec/controllers/controller_single_session_spec.rb b/spec/controllers/controller_single_session_spec.rb index 696d3336..e1109602 100644 --- a/spec/controllers/controller_single_session_spec.rb +++ b/spec/controllers/controller_single_session_spec.rb @@ -37,7 +37,7 @@ get :test_should_be_logged_in expect(session[:user_id]).to be_nil - expect(response).to be_successful + expect(response).not_to be_successful end it 'regenerates token on login' do