multi_session 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +110 -0
  4. data/Rakefile +12 -0
  5. data/lib/generators/multi_session/install_generator.rb +10 -0
  6. data/lib/generators/templates/multi_session.rb +7 -0
  7. data/lib/multi_session/helper.rb +21 -0
  8. data/lib/multi_session/railtie.rb +7 -0
  9. data/lib/multi_session/session.rb +59 -0
  10. data/lib/multi_session/version.rb +3 -0
  11. data/lib/multi_session.rb +15 -0
  12. data/spec/dummy/Rakefile +6 -0
  13. data/spec/dummy/app/assets/config/manifest.js +2 -0
  14. data/spec/dummy/app/assets/javascripts/application.js +14 -0
  15. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  16. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  17. data/spec/dummy/app/controllers/multi_session_test_controller.rb +18 -0
  18. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  19. data/spec/dummy/app/jobs/application_job.rb +2 -0
  20. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  21. data/spec/dummy/app/views/multi_session_test/some_action.html.erb +1 -0
  22. data/spec/dummy/bin/bundle +3 -0
  23. data/spec/dummy/bin/rails +4 -0
  24. data/spec/dummy/bin/rake +4 -0
  25. data/spec/dummy/bin/setup +25 -0
  26. data/spec/dummy/bin/update +25 -0
  27. data/spec/dummy/config/application.rb +29 -0
  28. data/spec/dummy/config/boot.rb +5 -0
  29. data/spec/dummy/config/credentials.yml.enc +1 -0
  30. data/spec/dummy/config/environment.rb +5 -0
  31. data/spec/dummy/config/environments/development.rb +40 -0
  32. data/spec/dummy/config/environments/production.rb +68 -0
  33. data/spec/dummy/config/environments/test.rb +36 -0
  34. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  35. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  36. data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
  37. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  38. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  39. data/spec/dummy/config/initializers/inflections.rb +16 -0
  40. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  41. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  42. data/spec/dummy/config/locales/en.yml +33 -0
  43. data/spec/dummy/config/master.key +1 -0
  44. data/spec/dummy/config/puma.rb +34 -0
  45. data/spec/dummy/config/routes.rb +5 -0
  46. data/spec/dummy/config/spring.rb +6 -0
  47. data/spec/dummy/config.ru +5 -0
  48. data/spec/dummy/log/development.log +0 -0
  49. data/spec/dummy/log/test.log +396 -0
  50. data/spec/dummy/public/404.html +67 -0
  51. data/spec/dummy/public/422.html +67 -0
  52. data/spec/dummy/public/500.html +66 -0
  53. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  54. data/spec/dummy/public/apple-touch-icon.png +0 -0
  55. data/spec/dummy/public/favicon.ico +0 -0
  56. data/spec/requests/multi_session_spec.rb +40 -0
  57. data/spec/spec_helper.rb +19 -0
  58. metadata +188 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e401b25f019731faaad3230a51406a7853c071837f9a134c7779bf2f1130841f
4
+ data.tar.gz: ef26f4a723075fcdb0b19406a787a8bd6fabd84e8fb4fac417c96a1be3c3d281
5
+ SHA512:
6
+ metadata.gz: 19f01b7549b920e6d25e6a072fabd846c80d6cd8f4ed60bb48ce75e88a58363abfc72ee9a8d42ba40aa62b02f01572294b67aac68f0eccb0711121d4711d9ad6
7
+ data.tar.gz: b2f5cd824bbc7e84310f352c08ce36ecb8450711ec5605846dbccc31e4b5c450d4bac6df72873afbc367d3284c7f0282f9004c9a3fb3b008d3d0d280b40f0c21
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018 Sean Huber
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ [![Gem Version](https://badge.fury.io/rb/multi_session.svg)](https://badge.fury.io/rb/multi_session)
2
+ [![Build Status](https://travis-ci.org/seanhuber/multi_session.svg?branch=master)](https://travis-ci.org/seanhuber/multi_session)
3
+ [![Coverage Status](https://coveralls.io/repos/github/seanhuber/multi_session/badge.svg?branch=master)](https://coveralls.io/github/seanhuber/multi_session?branch=master)
4
+
5
+ multi_session
6
+ ==============
7
+
8
+ `multi_session` is a Railtie that extends rails to provide the ability to have multiple sessions per user via encrypted cookies.
9
+
10
+ ## Motivation
11
+
12
+ Rails comes with a `session` helper method for storing user session information across HTTP requests. The default approach is to store that information in an encrypted cookie that gets passed as a header in each HTTP request/response. That cookie is encrypted/decrypted using the `secret_key_base` defined in `config/secrets.yml` (or `config/credentials.yml.enc` as of Rails version 5.2).
13
+
14
+ Rails' `session` is secure and works great but what if you'd like to share that session cookie with multiple Rails applications hosted across different subdomains? Various blogs and stackoverflow questions on the matter suggest sharing the same `secret_key_base` value amongst the multiple Rails applications. But what if these apps only want to share part of the session information? Or what if there are security concerns because `secret_key_base` is used for more than just encrypting session cookies? This `multi_session` railtie provides a helper method (named `multi_session`) similar to `session` except that it permits you to create multiple encrypted session cookies per user, each with their own secret key, giving you the flexibility to choose which session components could be shared with other Rails (and non-Rails) web applications.
15
+
16
+ ## Usage
17
+
18
+ `multi_session` is a helper method that gets added to your `ApplicationController` and is therefore accessible by all your controllers that subclass `ApplicationController`, as well all view templates. To create and read new sessions, use bracket notation (`[]` and `[]=`) like you would with `Hash` or hash-like objects.
19
+
20
+ For example:
21
+
22
+ ```ruby
23
+ # app/controllers/some_controller.rb
24
+
25
+ class SomeController < ApplicationController
26
+ def my_action
27
+ @user = User.find(params.require(:id))
28
+ multi_session[:global_user_session] = {
29
+ 'user_id' => @user.id,
30
+ 'email' => @user.email,
31
+ 'name' => @user.full_name
32
+ }
33
+ multi_session[:user_preferences] = {
34
+ 'enable_push_notifications' => true,
35
+ 'something_else' => false
36
+ }
37
+ end
38
+
39
+ def another_action
40
+ @user = User.find(multi_session[:global_user_session]['user_id'])
41
+ end
42
+ end
43
+ ```
44
+
45
+ In the example above, multi_session would create 2 encrypted cookies, one named `"global_user_session"` and the other named `"user_preferences"`. The key_base used to encrypt/decrypt these cookies would need to be defined in `config/secrets.yml` or `config/credentials.yml.enc` under a key named `:multi_session_keys`. Example:
46
+
47
+ ```yaml
48
+ # config/credentials.yml.enc
49
+ secret_key_base: # generated by Rails
50
+
51
+ multi_session_keys: # use `rake secret` to generate custom keys
52
+ global_user_session: # insert a new secret here
53
+ user_preferences: # insert a different secret here
54
+ ```
55
+
56
+ ## Installation
57
+
58
+ Add this line to your application's Gemfile:
59
+
60
+ ```ruby
61
+ gem 'multi_session', '~> 1.1'
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ For the current version `multi_session`, there these are the configuration values that can optionally be set:
67
+
68
+ | Config option | Type | Description |
69
+ |---------------------------------------|-------------------------|-------------------------------------------------------|
70
+ | `expires` | ActiveSupport::Duration | expiration period for `multi_session` cookies/values |
71
+ | `authenticated_encrypted_cookie_salt` | String | Salt used to derive key for GCM encryption |
72
+
73
+
74
+ To configure `multi_session`, first generate an initializer using the built-in rails generator:
75
+
76
+ ```
77
+ rails g multi_session:install
78
+ ```
79
+
80
+ Then open and edit `config/initializers/multi_session.rb`:
81
+
82
+ ```ruby
83
+ # config/initializers/multi_session.rb
84
+
85
+ MultiSession.setup do |config|
86
+ # Uncomment to force multi_session cookies to expire after a period of time
87
+ config.expires = 30.minutes
88
+
89
+ # Salt used to derive key for GCM encryption. Default value is 'multi session authenticated encrypted cookie'
90
+ config.authenticated_encrypted_cookie_salt = 'my multi session salt value'
91
+ end
92
+ ```
93
+
94
+ ## Security
95
+
96
+ `multi_session` does not introduce any novel security mechanisms. Encryptions/decryption is done using `ActiveSupport::MessageEncryptor` in the same manner that Rails encrypts the `session` cookie in Rails 5.2 (see https://github.com/rails/rails/blob/5fb4703471ffb11dab9aa3855daeef9f592f6388/actionpack/lib/action_dispatch/middleware/cookies.rb).
97
+
98
+ The default cipher for encrypting messages in Rails 5.2 is `AES-256-GCM`, which is the same as what `multi_session` uses.
99
+
100
+ There are many good writeups on the web (such as https://guides.rubyonrails.org/security.html, https://www.justinweiss.com/articles/how-rails-sessions-work/, and https://www.theodinproject.com/courses/ruby-on-rails/lessons/sessions-cookies-and-authentication) that go into detail of how Rails addresses session security concerns and I encourage all app developers to spend some time educating themselves on how Rails sessions work.
101
+
102
+ The implementation of `multi_session` is actually quite simple because it leans heavily on existing Rails functionality. The nuts and bolts are primarily coded in `lib/multi_session/session.rb` (https://github.com/seanhuber/multi_session/blob/b0211d714d995dc01eb817e52f5dc78e52120bf0/lib/multi_session/session.rb). It's a small file, please do your own code-audit before using this gem. :wink:
103
+
104
+ ## Contributing
105
+
106
+ Pull requests and issue reports are welcome!
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'bundler/gem_tasks'
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ task default: :spec
@@ -0,0 +1,10 @@
1
+ module MultiSession
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../../templates', __FILE__)
4
+
5
+ desc 'creates a config/initializers/multi_session.rb file for configuring multi_session'
6
+ def install
7
+ template 'multi_session.rb', 'config/initializers/multi_session.rb'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ MultiSession.setup do |config|
2
+ # Uncomment to force multi_session cookies to expire after a period of time
3
+ # config.expires = 30.minutes
4
+
5
+ # Salt used to derive key for GCM encryption. Default value is 'multi session authenticated encrypted cookie'
6
+ # config.authenticated_encrypted_cookie_salt = 'multi session authenticated encrypted cookie'
7
+ end
@@ -0,0 +1,21 @@
1
+ module MultiSession
2
+ module Helper
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :multi_session if defined?(helper_method)
7
+ after_action :slide_multi_session
8
+ end
9
+
10
+ private
11
+
12
+ def multi_session
13
+ Session.new cookies
14
+ end
15
+
16
+ def slide_multi_session
17
+ return unless MultiSession.expires.present?
18
+ Session.new(cookies).update_expiration
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module MultiSession
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'multi_session.configure_rails_initialization' do |app|
4
+ ActionController::Base.send :include, MultiSession::Helper
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,59 @@
1
+ module MultiSession
2
+ class Session
3
+ def initialize cookies
4
+ @cookies = cookies
5
+ end
6
+
7
+ def [](key)
8
+ return nil unless @cookies[key.to_s].present?
9
+ session = ActiveSupport::JSON.decode encryptor(key).decrypt_and_verify(@cookies[key])
10
+ session['value'] # TODO: add ability to let developer retrieve the session_id
11
+ end
12
+
13
+ def []=(key, value)
14
+ previous_session = self[key]
15
+ session_id = if previous_session && previous_session['session_id'].present?
16
+ previous_session['session_id']
17
+ else
18
+ SecureRandom.hex(16).encode Encoding::UTF_8
19
+ end
20
+
21
+ new_session = {
22
+ 'session_id' => session_id,
23
+ 'value' => value
24
+ }
25
+ expiry_options = MultiSession.expires.present? ? {expires_at: Time.now + MultiSession.expires} : {}
26
+ encrypted_and_signed_value = encryptor(key).encrypt_and_sign ActiveSupport::JSON.encode(new_session), expiry_options
27
+
28
+ raise ActionDispatch::Cookies::CookieOverflow if encrypted_and_signed_value.bytesize > ActionDispatch::Cookies::MAX_COOKIE_SIZE
29
+
30
+ @cookies[key.to_s] = {value: encrypted_and_signed_value}.merge(MultiSession.expires.present? ? {expires: MultiSession.expires} : {})
31
+ nil
32
+ end
33
+
34
+ def clear
35
+ @cookies.clear
36
+ end
37
+
38
+ def update_expiration
39
+ Rails.application.credentials[:multi_session_keys].each_key do |key|
40
+ self[key] = self[key] # decrypt and re-encrypt to force expires_at to update
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def encryptor key
47
+ secret_key_base = Rails.application.credentials[:multi_session_keys][key.to_sym]
48
+ raise ArgumentError.new("Rails.application.credentials[:multi_session_keys][:'#{key}'] has not been set.") unless secret_key_base.present?
49
+
50
+ encrypted_cookie_cipher = 'aes-256-gcm'
51
+ key_generator = ActiveSupport::CachingKeyGenerator.new ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
52
+ key_len = ActiveSupport::MessageEncryptor.key_len encrypted_cookie_cipher
53
+ salt = 'authenticated encrypted cookie'
54
+ secret = key_generator.generate_key(MultiSession.authenticated_encrypted_cookie_salt, key_len)
55
+
56
+ ActiveSupport::MessageEncryptor.new secret, cipher: encrypted_cookie_cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module MultiSession
2
+ VERSION = '1.1.0'
3
+ end
@@ -0,0 +1,15 @@
1
+ require 'multi_session/helper'
2
+ require 'multi_session/railtie'
3
+ require 'multi_session/session'
4
+
5
+ module MultiSession
6
+ mattr_accessor :authenticated_encrypted_cookie_salt
7
+ @@authenticated_encrypted_cookie_salt = 'multi session authenticated encrypted cookie'
8
+
9
+ mattr_accessor :expires
10
+ @@expires = nil
11
+
12
+ def self.setup
13
+ yield self
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative 'config/application'
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,2 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,14 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require rails-ujs
14
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,18 @@
1
+ class MultiSessionTestController < ApplicationController
2
+ def some_action
3
+ end
4
+
5
+ def encrypt_multi_sessions
6
+ params.require(:session_values).permit!.each do |session_key, value|
7
+ multi_session[session_key] = value
8
+ end
9
+ head :ok
10
+ end
11
+
12
+ def decrypt_multi_sessions
13
+ session_values = params.require(:session_keys).map do |key|
14
+ "#{key}-#{multi_session[key]}"
15
+ end.join(',')
16
+ render inline: session_values
17
+ end
18
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag 'application', media: 'all' %>
9
+ </head>
10
+
11
+ <body>
12
+ <%= yield %>
13
+ </body>
14
+ </html>
@@ -0,0 +1 @@
1
+ <h1>hello world!</h1>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../config/application', __dir__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+ include FileUtils
4
+
5
+ # path to your application root.
6
+ APP_ROOT = File.expand_path('..', __dir__)
7
+
8
+ def system!(*args)
9
+ system(*args) || abort("\n== Command #{args} failed ==")
10
+ end
11
+
12
+ chdir APP_ROOT do
13
+ # This script is a starting point to setup your application.
14
+ # Add necessary setup steps to this file.
15
+
16
+ puts '== Installing dependencies =='
17
+ system! 'gem install bundler --conservative'
18
+ system('bundle check') || system!('bundle install')
19
+
20
+ puts "\n== Removing old logs and tempfiles =="
21
+ system! 'bin/rails log:clear tmp:clear'
22
+
23
+ puts "\n== Restarting application server =="
24
+ system! 'bin/rails restart'
25
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+ include FileUtils
4
+
5
+ # path to your application root.
6
+ APP_ROOT = File.expand_path('..', __dir__)
7
+
8
+ def system!(*args)
9
+ system(*args) || abort("\n== Command #{args} failed ==")
10
+ end
11
+
12
+ chdir APP_ROOT do
13
+ # This script is a way to update your development environment automatically.
14
+ # Add necessary update steps to this file.
15
+
16
+ puts '== Installing dependencies =='
17
+ system! 'gem install bundler --conservative'
18
+ system('bundle check') || system!('bundle install')
19
+
20
+ puts "\n== Removing old logs and tempfiles =="
21
+ system! 'bin/rails log:clear tmp:clear'
22
+
23
+ puts "\n== Restarting application server =="
24
+ system! 'bin/rails restart'
25
+ end