From 2211e8d1cd6eb97a8a04e24c1fea7031a201edb5 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Fri, 23 Jun 2017 18:50:53 +0200 Subject: [PATCH] Revocable sessions (#3616) * feat: Revocable sessions * fix: Tests using sign_in * feat: Configuration entry for the maximum number of session activations --- app/models/session_activation.rb | 38 +++++++++++++++++++ app/models/user.rb | 14 +++++++ config/initializers/devise.rb | 16 ++++++++ config/initializers/session_activations.rb | 5 +++ ...170623152212_create_session_activations.rb | 13 +++++++ db/schema.rb | 11 +++++- .../session_activation_fabricator.rb | 4 ++ spec/models/session_activation_spec.rb | 5 +++ spec/rails_helper.rb | 11 ++++++ 9 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 app/models/session_activation.rb create mode 100644 config/initializers/session_activations.rb create mode 100644 db/migrate/20170623152212_create_session_activations.rb create mode 100644 spec/fabricators/session_activation_fabricator.rb create mode 100644 spec/models/session_activation_spec.rb diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb new file mode 100644 index 00000000..71e9f023 --- /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 ca11f2f5..fccf1089 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 4754c2c8..6d3a73ef 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 00000000..ff3efc85 --- /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 00000000..81c77613 --- /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 2f12c730..b6aceb93 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 00000000..46050bda --- /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 00000000..49c72fbd --- /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 c9bdc8ad..31c94b1e 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