Revocable sessions (#3616)
* feat: Revocable sessions * fix: Tests using sign_in * feat: Configuration entry for the maximum number of session activations
This commit is contained in:
		
				
					committed by
					
						
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							3783cadf2d
						
					
				
				
					commit
					2211e8d1cd
				
			
							
								
								
									
										38
									
								
								app/models/session_activation.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/models/session_activation.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					# == Schema Information
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Table name: session_activations
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  id         :integer          not null, primary key
 | 
				
			||||||
 | 
					#  user_id    :integer          not null
 | 
				
			||||||
 | 
					#  session_id :string           not null
 | 
				
			||||||
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SessionActivation < ApplicationRecord
 | 
				
			||||||
 | 
					  LIMIT = Rails.configuration.x.max_session_activations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def self.active?(id)
 | 
				
			||||||
 | 
					    id && where(session_id: id).exists?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def self.activate(id)
 | 
				
			||||||
 | 
					    activation = create!(session_id: id)
 | 
				
			||||||
 | 
					    purge_old
 | 
				
			||||||
 | 
					    activation
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def self.deactivate(id)
 | 
				
			||||||
 | 
					    return unless id
 | 
				
			||||||
 | 
					    where(session_id: id).destroy_all
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def self.purge_old
 | 
				
			||||||
 | 
					    order('created_at desc').offset(LIMIT).destroy_all
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def self.exclusive(id)
 | 
				
			||||||
 | 
					    where('session_id != ?', id).destroy_all
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -63,6 +63,8 @@ class User < ApplicationRecord
 | 
				
			|||||||
  # handle this itself, and this can be removed from our User class.
 | 
					  # handle this itself, and this can be removed from our User class.
 | 
				
			||||||
  attribute :otp_secret
 | 
					  attribute :otp_secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :session_activations, dependent: :destroy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def confirmed?
 | 
					  def confirmed?
 | 
				
			||||||
    confirmed_at.present?
 | 
					    confirmed_at.present?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -89,6 +91,18 @@ class User < ApplicationRecord
 | 
				
			|||||||
    settings.auto_play_gif
 | 
					    settings.auto_play_gif
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def activate_session
 | 
				
			||||||
 | 
					    session_activations.activate(SecureRandom.hex).session_id
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def exclusive_session(id)
 | 
				
			||||||
 | 
					    session_activations.exclusive(id)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def session_active?(id)
 | 
				
			||||||
 | 
					    session_activations.active? id
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected
 | 
					  protected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def send_devise_notification(notification, *args)
 | 
					  def send_devise_notification(notification, *args)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,19 @@
 | 
				
			|||||||
 | 
					Warden::Manager.after_set_user except: :fetch do |user, warden|
 | 
				
			||||||
 | 
					  SessionActivation.deactivate warden.raw_session['auth_id']
 | 
				
			||||||
 | 
					  warden.raw_session['auth_id'] = user.activate_session
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Warden::Manager.after_fetch do |user, warden|
 | 
				
			||||||
 | 
					  unless user.session_active?(warden.raw_session['auth_id'])
 | 
				
			||||||
 | 
					    warden.logout
 | 
				
			||||||
 | 
					    throw :warden, message: :unauthenticated
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Warden::Manager.before_logout do |_, warden|
 | 
				
			||||||
 | 
					  SessionActivation.deactivate warden.raw_session['auth_id']
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Devise.setup do |config|
 | 
					Devise.setup do |config|
 | 
				
			||||||
  config.warden do |manager|
 | 
					  config.warden do |manager|
 | 
				
			||||||
    manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
 | 
					    manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								config/initializers/session_activations.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								config/initializers/session_activations.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Rails.application.configure do
 | 
				
			||||||
 | 
					  config.x.max_session_activations = ENV['MAX_SESSION_ACTIVATIONS'] || 10
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										13
									
								
								db/migrate/20170623152212_create_session_activations.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								db/migrate/20170623152212_create_session_activations.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					class CreateSessionActivations < ActiveRecord::Migration[5.1]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    create_table :session_activations do |t|
 | 
				
			||||||
 | 
					      t.integer :user_id,   null: false
 | 
				
			||||||
 | 
					      t.string :session_id, null: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      t.timestamps
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    add_index :session_activations, :user_id
 | 
				
			||||||
 | 
					    add_index :session_activations, :session_id, unique: true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										11
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								db/schema.rb
									
									
									
									
									
								
							@@ -10,7 +10,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# It's strongly recommended that you check this file into your version control system.
 | 
					# It's strongly recommended that you check this file into your version control system.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ActiveRecord::Schema.define(version: 20170610000000) do
 | 
					ActiveRecord::Schema.define(version: 20170623152212) do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # These are extensions that must be enabled in order to support this database
 | 
					  # These are extensions that must be enabled in order to support this database
 | 
				
			||||||
  enable_extension "plpgsql"
 | 
					  enable_extension "plpgsql"
 | 
				
			||||||
@@ -250,6 +250,15 @@ ActiveRecord::Schema.define(version: 20170610000000) do
 | 
				
			|||||||
    t.index ["target_account_id"], name: "index_reports_on_target_account_id"
 | 
					    t.index ["target_account_id"], name: "index_reports_on_target_account_id"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  create_table "session_activations", force: :cascade do |t|
 | 
				
			||||||
 | 
					    t.integer "user_id", null: false
 | 
				
			||||||
 | 
					    t.string "session_id", null: false
 | 
				
			||||||
 | 
					    t.datetime "created_at", null: false
 | 
				
			||||||
 | 
					    t.datetime "updated_at", null: false
 | 
				
			||||||
 | 
					    t.index ["session_id"], name: "index_session_activations_on_session_id", unique: true
 | 
				
			||||||
 | 
					    t.index ["user_id"], name: "index_session_activations_on_user_id"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  create_table "settings", id: :serial, force: :cascade do |t|
 | 
					  create_table "settings", id: :serial, force: :cascade do |t|
 | 
				
			||||||
    t.string "var", null: false
 | 
					    t.string "var", null: false
 | 
				
			||||||
    t.text "value"
 | 
					    t.text "value"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								spec/fabricators/session_activation_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spec/fabricators/session_activation_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					Fabricator(:session_activation) do
 | 
				
			||||||
 | 
					  user_id    1
 | 
				
			||||||
 | 
					  session_id "MyString"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										5
									
								
								spec/models/session_activation_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/session_activation_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RSpec.describe SessionActivation, type: :model do
 | 
				
			||||||
 | 
					  pending "add some examples to (or delete) #{__FILE__}"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -16,6 +16,17 @@ WebMock.disable_net_connect!
 | 
				
			|||||||
Sidekiq::Testing.inline!
 | 
					Sidekiq::Testing.inline!
 | 
				
			||||||
Sidekiq::Logging.logger = nil
 | 
					Sidekiq::Logging.logger = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Devise::Test::ControllerHelpers.module_eval do
 | 
				
			||||||
 | 
					  alias_method :original_sign_in, :sign_in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def sign_in(resource, deprecated = nil, scope: nil)
 | 
				
			||||||
 | 
					    original_sign_in(resource, scope: scope)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SessionActivation.deactivate warden.raw_session["auth_id"]
 | 
				
			||||||
 | 
					    warden.raw_session["auth_id"] = resource.activate_session
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RSpec.configure do |config|
 | 
					RSpec.configure do |config|
 | 
				
			||||||
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
 | 
					  config.fixture_path = "#{::Rails.root}/spec/fixtures"
 | 
				
			||||||
  config.use_transactional_fixtures = true
 | 
					  config.use_transactional_fixtures = true
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user