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:
		
							parent
							
								
									3783cadf2d
								
							
						
					
					
						commit
						2211e8d1cd
					
				
					 9 changed files with 116 additions and 1 deletions
				
			
		
							
								
								
									
										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. | ||||
|   attribute :otp_secret | ||||
| 
 | ||||
|   has_many :session_activations, dependent: :destroy | ||||
| 
 | ||||
|   def confirmed? | ||||
|     confirmed_at.present? | ||||
|   end | ||||
|  | @ -89,6 +91,18 @@ class User < ApplicationRecord | |||
|     settings.auto_play_gif | ||||
|   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 | ||||
| 
 | ||||
|   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| | ||||
|   config.warden do |manager| | ||||
|     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. | ||||
| 
 | ||||
| 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 | ||||
|   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" | ||||
|   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| | ||||
|     t.string "var", null: false | ||||
|     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::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| | ||||
|   config.fixture_path = "#{::Rails.root}/spec/fixtures" | ||||
|   config.use_transactional_fixtures = true | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue