about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSorin Davidoi <sorin.davidoi@gmail.com>2017-06-23 18:50:53 +0200
committerEugen Rochko <eugen@zeonfederated.com>2017-06-23 18:50:53 +0200
commit2211e8d1cd6eb97a8a04e24c1fea7031a201edb5 (patch)
treeb235a97b1c0f73475449c2313823dc6d599f31bb
parent3783cadf2d7a2b7ace078d1d337645f53c190c69 (diff)
Revocable sessions (#3616)
* feat: Revocable sessions

* fix: Tests using sign_in

* feat: Configuration entry for the maximum number of session activations
-rw-r--r--app/models/session_activation.rb38
-rw-r--r--app/models/user.rb14
-rw-r--r--config/initializers/devise.rb16
-rw-r--r--config/initializers/session_activations.rb5
-rw-r--r--db/migrate/20170623152212_create_session_activations.rb13
-rw-r--r--db/schema.rb11
-rw-r--r--spec/fabricators/session_activation_fabricator.rb4
-rw-r--r--spec/models/session_activation_spec.rb5
-rw-r--r--spec/rails_helper.rb11
9 files changed, 116 insertions, 1 deletions
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
new file mode 100644
index 000000000..71e9f023c
--- /dev/null
+++ b/app/models/session_activation.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index ca11f2f5d..fccf1089b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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)
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 4754c2c8c..6d3a73ef6 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -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
diff --git a/config/initializers/session_activations.rb b/config/initializers/session_activations.rb
new file mode 100644
index 000000000..ff3efc852
--- /dev/null
+++ b/config/initializers/session_activations.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+  config.x.max_session_activations = ENV['MAX_SESSION_ACTIVATIONS'] || 10
+end
diff --git a/db/migrate/20170623152212_create_session_activations.rb b/db/migrate/20170623152212_create_session_activations.rb
new file mode 100644
index 000000000..81c776135
--- /dev/null
+++ b/db/migrate/20170623152212_create_session_activations.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 2f12c7308..b6aceb930 100644
--- a/db/schema.rb
+++ b/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"
diff --git a/spec/fabricators/session_activation_fabricator.rb b/spec/fabricators/session_activation_fabricator.rb
new file mode 100644
index 000000000..46050bdab
--- /dev/null
+++ b/spec/fabricators/session_activation_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:session_activation) do
+  user_id    1
+  session_id "MyString"
+end
diff --git a/spec/models/session_activation_spec.rb b/spec/models/session_activation_spec.rb
new file mode 100644
index 000000000..49c72fbd4
--- /dev/null
+++ b/spec/models/session_activation_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe SessionActivation, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index c9bdc8add..31c94b1e4 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -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