quo_vadis 1.3.2 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (190) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +11 -7
  3. data/CHANGELOG.md +33 -0
  4. data/Gemfile +11 -1
  5. data/LICENSE.txt +21 -0
  6. data/README.md +434 -127
  7. data/Rakefile +14 -16
  8. data/app/controllers/quo_vadis/confirmations_controller.rb +56 -0
  9. data/app/controllers/quo_vadis/logs_controller.rb +20 -0
  10. data/app/controllers/quo_vadis/password_resets_controller.rb +65 -0
  11. data/app/controllers/quo_vadis/passwords_controller.rb +26 -0
  12. data/app/controllers/quo_vadis/recovery_codes_controller.rb +54 -0
  13. data/app/controllers/quo_vadis/sessions_controller.rb +50 -132
  14. data/app/controllers/quo_vadis/totps_controller.rb +72 -0
  15. data/app/controllers/quo_vadis/twofas_controller.rb +26 -0
  16. data/app/mailers/quo_vadis/mailer.rb +73 -0
  17. data/app/models/quo_vadis/account.rb +59 -0
  18. data/app/models/quo_vadis/account_confirmation_token.rb +17 -0
  19. data/app/models/quo_vadis/log.rb +57 -0
  20. data/app/models/quo_vadis/password.rb +52 -0
  21. data/app/models/quo_vadis/password_reset_token.rb +17 -0
  22. data/app/models/quo_vadis/recovery_code.rb +26 -0
  23. data/app/models/quo_vadis/session.rb +55 -0
  24. data/app/models/quo_vadis/token.rb +42 -0
  25. data/app/models/quo_vadis/totp.rb +56 -0
  26. data/bin/console +15 -0
  27. data/bin/rails +21 -0
  28. data/bin/setup +8 -0
  29. data/config/locales/quo_vadis.en.yml +51 -18
  30. data/config/routes.rb +40 -12
  31. data/db/migrate/202102150904_setup.rb +48 -0
  32. data/lib/generators/quo_vadis/install_generator.rb +4 -23
  33. data/lib/quo_vadis.rb +100 -106
  34. data/lib/quo_vadis/controller.rb +227 -0
  35. data/lib/quo_vadis/crypt.rb +43 -0
  36. data/lib/quo_vadis/current_request_details.rb +11 -0
  37. data/lib/quo_vadis/defaults.rb +18 -0
  38. data/lib/quo_vadis/encrypted_type.rb +17 -0
  39. data/lib/quo_vadis/engine.rb +9 -11
  40. data/lib/quo_vadis/hmacable.rb +26 -0
  41. data/lib/quo_vadis/ip_masking.rb +31 -0
  42. data/lib/quo_vadis/model.rb +86 -0
  43. data/lib/quo_vadis/version.rb +3 -1
  44. data/quo_vadis.gemspec +20 -24
  45. data/test/dummy/README.markdown +1 -0
  46. data/test/dummy/Rakefile +2 -6
  47. data/test/dummy/app/controllers/application_controller.rb +0 -1
  48. data/test/dummy/app/controllers/articles_controller.rb +8 -11
  49. data/test/dummy/app/controllers/sign_ups_controller.rb +42 -0
  50. data/test/dummy/app/controllers/users_controller.rb +13 -5
  51. data/test/dummy/app/models/application_record.rb +3 -0
  52. data/test/dummy/app/models/article.rb +2 -1
  53. data/test/dummy/app/models/person.rb +5 -2
  54. data/test/dummy/app/models/user.rb +4 -1
  55. data/test/dummy/app/views/articles/also_secret.html.erb +1 -0
  56. data/test/dummy/app/views/articles/index.html.erb +1 -1
  57. data/test/dummy/app/views/articles/secret.html.erb +1 -0
  58. data/test/dummy/app/views/articles/very_secret.html.erb +2 -0
  59. data/test/dummy/app/views/layouts/application.html.erb +41 -25
  60. data/test/dummy/app/views/quo_vadis/confirmations/edit.html.erb +10 -0
  61. data/test/dummy/app/views/quo_vadis/confirmations/index.html.erb +5 -0
  62. data/test/dummy/app/views/quo_vadis/confirmations/new.html.erb +16 -0
  63. data/test/dummy/app/views/quo_vadis/logs/index.html.erb +28 -0
  64. data/test/dummy/app/views/quo_vadis/mailer/account_confirmation.text.erb +4 -0
  65. data/test/dummy/app/views/quo_vadis/mailer/email_change_notification.text.erb +8 -0
  66. data/test/dummy/app/views/quo_vadis/mailer/identifier_change_notification.text.erb +8 -0
  67. data/test/dummy/app/views/quo_vadis/mailer/password_change_notification.text.erb +8 -0
  68. data/test/dummy/app/views/quo_vadis/mailer/password_reset_notification.text.erb +8 -0
  69. data/test/dummy/app/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb +8 -0
  70. data/test/dummy/app/views/quo_vadis/mailer/reset_password.text.erb +4 -0
  71. data/test/dummy/app/views/quo_vadis/mailer/totp_reuse_notification.text.erb +6 -0
  72. data/test/dummy/app/views/quo_vadis/mailer/totp_setup_notification.text.erb +8 -0
  73. data/test/dummy/app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb +8 -0
  74. data/test/dummy/app/views/quo_vadis/password_resets/edit.html.erb +25 -0
  75. data/test/dummy/app/views/quo_vadis/password_resets/index.html.erb +5 -0
  76. data/test/dummy/app/views/quo_vadis/password_resets/new.html.erb +12 -0
  77. data/test/dummy/app/views/quo_vadis/passwords/edit.html.erb +30 -0
  78. data/test/dummy/app/views/quo_vadis/recovery_codes/challenge.html.erb +11 -0
  79. data/test/dummy/app/views/quo_vadis/recovery_codes/index.html.erb +25 -0
  80. data/test/dummy/app/views/quo_vadis/sessions/index.html.erb +26 -0
  81. data/test/dummy/app/views/quo_vadis/sessions/new.html.erb +24 -0
  82. data/test/dummy/app/views/quo_vadis/totps/challenge.html.erb +11 -0
  83. data/test/dummy/app/views/quo_vadis/totps/new.html.erb +17 -0
  84. data/test/dummy/app/views/quo_vadis/twofas/show.html.erb +20 -0
  85. data/test/dummy/app/views/sign_ups/new.html.erb +37 -0
  86. data/test/dummy/app/views/sign_ups/show.html.erb +5 -0
  87. data/test/dummy/app/views/users/new.html.erb +32 -9
  88. data/test/dummy/config.ru +6 -3
  89. data/test/dummy/config/application.rb +18 -9
  90. data/test/dummy/config/boot.rb +3 -9
  91. data/test/dummy/config/database.yml +2 -14
  92. data/test/dummy/config/environment.rb +2 -3
  93. data/test/dummy/config/initializers/quo_vadis.rb +5 -82
  94. data/test/dummy/config/routes.rb +11 -3
  95. data/test/dummy/db/migrate/202102121932_create_users.rb +10 -0
  96. data/test/dummy/db/migrate/202102121935_create_people.rb +10 -0
  97. data/test/dummy/db/schema.rb +80 -21
  98. data/test/fixtures/quo_vadis/mailer/account_confirmation.text +4 -0
  99. data/test/fixtures/quo_vadis/mailer/email_change_notification.text +8 -0
  100. data/test/fixtures/quo_vadis/mailer/identifier_change_notification.text +8 -0
  101. data/test/fixtures/quo_vadis/mailer/password_change_notification.text +8 -0
  102. data/test/fixtures/quo_vadis/mailer/password_reset_notification.text +8 -0
  103. data/test/fixtures/quo_vadis/mailer/recovery_codes_generation_notification.text +8 -0
  104. data/test/fixtures/quo_vadis/mailer/reset_password.text +4 -0
  105. data/test/fixtures/quo_vadis/mailer/totp_reuse_notification.text +6 -0
  106. data/test/fixtures/quo_vadis/mailer/totp_setup_notification.text +8 -0
  107. data/test/fixtures/quo_vadis/mailer/twofa_deactivated_notification.text +8 -0
  108. data/test/integration/account_confirmation_test.rb +112 -0
  109. data/test/integration/controller_test.rb +280 -0
  110. data/test/integration/logging_test.rb +235 -0
  111. data/test/integration/password_change_test.rb +93 -0
  112. data/test/integration/password_login_test.rb +125 -0
  113. data/test/integration/password_reset_test.rb +136 -0
  114. data/test/integration/recovery_codes_test.rb +48 -0
  115. data/test/integration/sessions_test.rb +86 -0
  116. data/test/integration/sign_up_test.rb +26 -12
  117. data/test/integration/totps_test.rb +96 -0
  118. data/test/integration/twofa_test.rb +82 -0
  119. data/test/mailers/mailer_test.rb +200 -0
  120. data/test/models/account_test.rb +34 -0
  121. data/test/models/crypt_test.rb +22 -0
  122. data/test/models/log_test.rb +16 -0
  123. data/test/models/mask_ip_test.rb +27 -0
  124. data/test/models/model_test.rb +66 -0
  125. data/test/models/password_test.rb +163 -0
  126. data/test/models/recovery_code_test.rb +54 -0
  127. data/test/models/session_test.rb +67 -0
  128. data/test/models/token_test.rb +70 -0
  129. data/test/models/totp_test.rb +68 -0
  130. data/test/quo_vadis_test.rb +39 -3
  131. data/test/test_helper.rb +42 -72
  132. metadata +122 -193
  133. data/app/controllers/controller_mixin.rb +0 -109
  134. data/app/mailers/quo_vadis/notifier.rb +0 -30
  135. data/app/models/model_mixin.rb +0 -128
  136. data/lib/generators/quo_vadis/templates/migration.rb.erb +0 -18
  137. data/lib/generators/quo_vadis/templates/quo_vadis.rb.erb +0 -96
  138. data/test/dummy/.gitignore +0 -2
  139. data/test/dummy/app/helpers/application_helper.rb +0 -2
  140. data/test/dummy/app/helpers/articles_helper.rb +0 -2
  141. data/test/dummy/app/views/articles/new.html.erb +0 -11
  142. data/test/dummy/app/views/layouts/sessions.html.erb +0 -3
  143. data/test/dummy/app/views/quo_vadis/notifier/change_password.text.erb +0 -9
  144. data/test/dummy/app/views/quo_vadis/notifier/invite.text.erb +0 -8
  145. data/test/dummy/app/views/sessions/edit.html.erb +0 -11
  146. data/test/dummy/app/views/sessions/forgotten.html.erb +0 -13
  147. data/test/dummy/app/views/sessions/invite.html.erb +0 -31
  148. data/test/dummy/app/views/sessions/new.html.erb +0 -15
  149. data/test/dummy/config/environments/development.rb +0 -26
  150. data/test/dummy/config/environments/production.rb +0 -49
  151. data/test/dummy/config/environments/test.rb +0 -37
  152. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  153. data/test/dummy/config/initializers/inflections.rb +0 -10
  154. data/test/dummy/config/initializers/mime_types.rb +0 -5
  155. data/test/dummy/config/initializers/rack_patch.rb +0 -16
  156. data/test/dummy/config/initializers/secret_token.rb +0 -7
  157. data/test/dummy/config/initializers/session_store.rb +0 -8
  158. data/test/dummy/config/locales/en.yml +0 -5
  159. data/test/dummy/config/locales/quo_vadis.en.yml +0 -21
  160. data/test/dummy/db/migrate/20110124125037_create_users.rb +0 -13
  161. data/test/dummy/db/migrate/20110124131535_create_articles.rb +0 -14
  162. data/test/dummy/db/migrate/20110127094709_add_authentication_to_users.rb +0 -18
  163. data/test/dummy/db/migrate/20111004112209_create_people.rb +0 -13
  164. data/test/dummy/db/migrate/20111004132342_add_authentication_to_people.rb +0 -18
  165. data/test/dummy/public/404.html +0 -26
  166. data/test/dummy/public/422.html +0 -26
  167. data/test/dummy/public/500.html +0 -26
  168. data/test/dummy/public/javascripts/application.js +0 -2
  169. data/test/dummy/public/javascripts/controls.js +0 -965
  170. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  171. data/test/dummy/public/javascripts/effects.js +0 -1123
  172. data/test/dummy/public/javascripts/prototype.js +0 -6001
  173. data/test/dummy/public/javascripts/rails.js +0 -175
  174. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  175. data/test/dummy/script/rails +0 -6
  176. data/test/integration/activation_test.rb +0 -108
  177. data/test/integration/authenticate_test.rb +0 -39
  178. data/test/integration/blocked_test.rb +0 -23
  179. data/test/integration/config_test.rb +0 -132
  180. data/test/integration/cookie_test.rb +0 -67
  181. data/test/integration/csrf_test.rb +0 -41
  182. data/test/integration/forgotten_test.rb +0 -93
  183. data/test/integration/helper_test.rb +0 -18
  184. data/test/integration/locale_test.rb +0 -197
  185. data/test/integration/navigation_test.rb +0 -7
  186. data/test/integration/sign_in_person_test.rb +0 -26
  187. data/test/integration/sign_in_test.rb +0 -24
  188. data/test/integration/sign_out_test.rb +0 -20
  189. data/test/support/integration_case.rb +0 -11
  190. data/test/unit/user_test.rb +0 -75
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuoVadis
4
+ class Token
5
+ extend Hmacable
6
+
7
+ class << self
8
+
9
+ def generate(account)
10
+ public_data = "#{account.id}-#{expires_at}"
11
+ data = data_for_hmac public_data, account
12
+ "#{public_data}--#{compute_hmac(data)}"
13
+ end
14
+
15
+ def find_account(token)
16
+ provided_public_data, provided_hmac = token.split '--'
17
+ id, expires_at = provided_public_data.split '-'
18
+ account = Account.find id
19
+ data = data_for_hmac provided_public_data, account
20
+ actual_hmac = compute_hmac data
21
+ return nil unless timing_safe_eql? provided_hmac, actual_hmac
22
+ return nil if expires_at.to_i < Time.current.to_i
23
+ account
24
+ rescue
25
+ nil
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :account
31
+
32
+ def expires_at
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def data_for_hmac(public_data, account)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rotp'
4
+ require 'rqrcode'
5
+
6
+ module QuoVadis
7
+ class Totp < ActiveRecord::Base
8
+ extend Hmacable
9
+
10
+ attribute :key, :qv_encrypted, default: -> { ROTP::Base32.random }
11
+ attribute :hmac_key, :string
12
+ attribute :provided_hmac_key, :string
13
+
14
+ belongs_to :account
15
+
16
+ validate :key_not_tampered_with, if: -> { provided_hmac_key.present? }
17
+ validates :key, presence: true
18
+
19
+
20
+ def qr_code
21
+ RQRCode::QRCode.new(
22
+ ROTP::TOTP.new(key, issuer: QuoVadis.app_name).provisioning_uri(account.identifier)
23
+ )
24
+ end
25
+
26
+
27
+ def hmac_key
28
+ self.class.compute_hmac key
29
+ end
30
+
31
+
32
+ # Returns true and saves the record if `otp` is valid, false otherwise.
33
+ def verify(otp)
34
+ if (_last_used_at = ROTP::TOTP.new(key).verify otp, after: last_used_at)
35
+ self.last_used_at = _last_used_at
36
+ save
37
+ else
38
+ false
39
+ end
40
+ end
41
+
42
+
43
+ def reused?(otp)
44
+ totp = ROTP::TOTP.new key
45
+ !totp.verify(otp, after: last_used_at) && totp.verify(otp)
46
+ end
47
+
48
+
49
+ private
50
+
51
+ def key_not_tampered_with
52
+ errors.add :key, :invalid unless self.class.timing_safe_eql? provided_hmac_key, hmac_key
53
+ end
54
+
55
+ end
56
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "quo_vadis"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/rails ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/quo_vadis/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12
+
13
+ require "active_model/railtie"
14
+ require "active_record/railtie"
15
+ # require "action_controller/railtie"
16
+ # require "action_view/railtie"
17
+ # require "action_mailer/railtie"
18
+ require "rails/test_unit/railtie"
19
+
20
+ # require "rails/all"
21
+ require "rails/engine/commands"
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -1,22 +1,55 @@
1
1
  en:
2
2
  quo_vadis:
3
3
  flash:
4
- sign_in:
5
- before: 'Please sign in first.'
6
- after: 'You have successfully signed in.'
7
- failed: 'Sorry, we did not recognise you.'
8
- blocked: 'Sorry, your account is blocked.'
4
+ login:
5
+ failed: Sorry, we did not recognise you.
6
+ success: Welcome back!
7
+ logout:
8
+ self: You have logged out.
9
+ other: You have logged out of the other session.
10
+ require_authentication: Please log in first.
11
+ password:
12
+ update: Your password has been changed.
13
+ password_reset:
14
+ create: A link to change your password has been emailed to you.
15
+ unknown: Either the link has expired or you have already reset your password.
16
+ reset: Your password has been changed and you are logged in.
17
+ confirmation:
18
+ create: A link to confirm your account has been emailed to you.
19
+ required: Please confirm your account first.
20
+ identifier: Sorry, your account could not be found. Please try again.
21
+ unknown: Either the link has expired or your account has already been confirmed.
22
+ confirmed: Thanks for confirming your account. You are now logged in.
23
+ totp:
24
+ unverified: Sorry, the code was incorrect. Please check your system clock is correct and try again.
25
+ setup: Please set up two factor authentication.
26
+ recovery_code:
27
+ unverified: Sorry, the code was incorrect. Please try again (you cannot reuse a code you have used before).
28
+ success:
29
+ zero: You have used up all your recovery codes now. Please generate a new set.
30
+ one: You have one recovery code left.
31
+ other: You have %{count} recovery codes left.
32
+ 2fa:
33
+ invalidated: You have invalidated your 2FA credentials and recovery codes.
9
34
 
10
- sign_out: 'You have successfully signed out.'
11
-
12
- forgotten:
13
- unknown: "Sorry, we did not recognise you."
14
- no_email: "Sorry, we don't have an email address for you."
15
- sent_email: "We've emailed you a link where you can change your password."
16
- invalid_token: "Sorry, this link isn't valid anymore."
17
- password_changed: "You have successfully changed your password and you're now signed in."
18
-
19
- activation:
20
- accepted: "Your account is active and you're now signed in."
21
- invalid_token: "Sorry, this link isn't valid anymore."
22
- invalid_credentials: "Sorry, we couldn't accept that username and/or password."
35
+ mailer:
36
+ password_reset:
37
+ subject: Change your password
38
+ confirmation:
39
+ subject: Please confirm your account
40
+ notification:
41
+ email_change: Your email address has been changed
42
+ identifier_change: Your %{identifier} has been changed
43
+ password_change: Your password has been changed
44
+ password_reset: Your password has been reset
45
+ totp_setup: Two-factor authentication was set up just now
46
+ totp_reuse: Your two-factor authentication code was reused just now
47
+ twofa_deactivated: Two-factor authentication was deactivated just now
48
+ recovery_codes_generation: Recovery codes have been generated for your account
49
+ activerecord:
50
+ errors:
51
+ models:
52
+ quo_vadis/password:
53
+ attributes:
54
+ password:
55
+ incorrect: is incorrect
data/config/routes.rb CHANGED
@@ -1,15 +1,43 @@
1
- Rails.application.routes.draw do
2
- scope :module => 'quo_vadis' do
3
- get 'sign-in' => 'sessions#new', :as => 'sign_in'
4
- post 'sign-in' => 'sessions#create', :as => 'sign_in'
5
- get 'sign-out' => 'sessions#destroy', :as => 'sign_out'
6
- get 'sign-in/forgotten' => 'sessions#forgotten', :as => 'forgotten_sign_in'
7
- post 'sign-in/forgotten' => 'sessions#forgotten', :as => 'forgotten_sign_in'
8
- constraints :token => /.+/ do
9
- get 'sign-in/change-password/:token' => 'sessions#edit', :as => 'change_password'
10
- put 'sign-in/change-password/:token' => 'sessions#update', :as => 'change_password'
11
- get 'sign-in/invite/:token' => 'sessions#invite', :as => 'invitation'
12
- post 'sign-in/accept/:token' => 'sessions#accept', :as => 'activation'
1
+ # frozen_string_literal: true
2
+
3
+ QuoVadis::Engine.routes.draw do
4
+ get 'login', to: 'sessions#new'
5
+ post 'login', to: 'sessions#create'
6
+ delete 'logout', to: 'sessions#destroy'
7
+
8
+ resources :logs, only: :index
9
+
10
+ resources :sessions, only: [:index, :destroy]
11
+
12
+ resource :password, only: [:edit, :update]
13
+
14
+ resources :password_resets, only: [:new, :create, :index]
15
+ get '/pwd-reset/:token', to: 'password_resets#edit', as: 'edit_password_reset'
16
+ put '/pwd-reset/:token', to: 'password_resets#update', as: 'password_reset'
17
+
18
+ resources :confirmations, only: [:new, :create, :index]
19
+ get '/confirm/:token', to: 'confirmations#edit', as: 'edit_confirmation'
20
+ put '/confirm/:token', to: 'confirmations#update', as: 'confirmation'
21
+
22
+ resources :totps, only: [:new, :create] do
23
+ collection do
24
+ get :challenge
25
+ post :authenticate
26
+ end
27
+ end
28
+
29
+ resources :recovery_codes, only: [:index] do
30
+ collection do
31
+ get :challenge
32
+ post :authenticate
33
+ post :generate
13
34
  end
14
35
  end
36
+
37
+ resource :twofa, path: '2fa'
38
+ end
39
+
40
+
41
+ Rails.application.routes.draw do
42
+ mount QuoVadis::Engine, at: QuoVadis.mount_point, as: :quo_vadis
15
43
  end
@@ -0,0 +1,48 @@
1
+ class Setup < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :qv_accounts do |t|
4
+ t.references :model, polymorphic: true, index: {unique: true}, null: false
5
+ t.string :identifier, index: {unique: true}, null: false
6
+ t.datetime :confirmed_at
7
+ t.timestamps
8
+ end
9
+
10
+ create_table :qv_passwords do |t|
11
+ t.references :account, foreign_key: {to_table: :qv_accounts}, null: false
12
+ t.string :password_digest, null: false
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :qv_sessions do |t|
17
+ t.references :account, foreign_key: {to_table: :qv_accounts}, null: false
18
+ t.string :ip, null: false
19
+ t.string :user_agent, null: false
20
+ t.datetime :lifetime_expires_at
21
+ t.datetime :last_seen_at
22
+ t.datetime :second_factor_at
23
+ t.timestamps
24
+ end
25
+
26
+ create_table :qv_totps do |t|
27
+ t.references :account, foreign_key: {to_table: :qv_accounts}, null: false
28
+ t.string :key, null: false
29
+ t.integer :last_used_at, null: false
30
+ t.timestamps
31
+ end
32
+
33
+ create_table :qv_recovery_codes do |t|
34
+ t.references :account, foreign_key: {to_table: :qv_accounts}, null: false
35
+ t.string :code_digest, null: false
36
+ t.timestamps
37
+ end
38
+
39
+ create_table :qv_logs do |t|
40
+ t.references :account, foreign_key: {to_table: :qv_accounts}
41
+ t.string :action, null: false
42
+ t.string :ip, null: false
43
+ t.json :metadata, null: false, default: {}
44
+ t.timestamps
45
+ end
46
+ end
47
+ end
48
+
@@ -1,29 +1,10 @@
1
- require 'rails/generators'
2
- require 'rails/generators/migration'
3
- require 'rails/generators/active_record/migration'
4
-
5
1
  module QuoVadis
6
2
  class InstallGenerator < Rails::Generators::Base
7
- include Rails::Generators::Migration
8
- extend ActiveRecord::Generators::Migration
9
-
10
- source_root File.expand_path('../templates', __FILE__)
11
- argument :model_name, :type => :string, :default => 'User'
12
-
13
- desc 'Copies an initializer, a locale file, and a migration to your application.'
3
+ source_root Pathname.new(__dir__) / '..' / '..' / '..' / 'test' / 'dummy' / 'app' / 'views' / 'quo_vadis'
14
4
 
15
-
16
- def copy_locale_file
17
- copy_file '../../../../config/locales/quo_vadis.en.yml', 'config/locales/quo_vadis.en.yml'
5
+ desc "Copy QuoVadis' views into your app."
6
+ def copy_views
7
+ directory '.', Pathname.new('app') / 'views' / 'quo_vadis'
18
8
  end
19
-
20
- def copy_initializer_file
21
- template 'quo_vadis.rb.erb', 'config/initializers/quo_vadis.rb'
22
- end
23
-
24
- def create_migration_file
25
- migration_template 'migration.rb.erb', "db/migrate/add_authentication_to_#{model_name.tableize}.rb"
26
- end
27
-
28
9
  end
29
10
  end
data/lib/quo_vadis.rb CHANGED
@@ -1,127 +1,121 @@
1
- require 'quo_vadis/engine'
2
- require 'active_support/core_ext/numeric/time'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module QuoVadis
5
4
 
6
- # The model we want to authenticate.
7
- mattr_accessor :model # :nodoc:
8
- @@model = 'User'
9
-
10
- def self.model_class # :nodoc
11
- @@model.constantize
12
- end
13
-
14
- def self.model_instance_name # :nodoc
15
- @@model.tableize.singularize # e.g. 'user'
16
- end
17
-
18
-
19
- #
20
- # Sign in
21
- #
22
-
23
- # The URL to redirect the user to after s/he signs in.
24
- mattr_accessor :signed_in_url
25
- @@signed_in_url = :root
26
-
27
- # Whether the `:signed_in_url` should override the URL the user was trying
28
- # to reach when they were made to authenticate.
29
- mattr_accessor :override_original_url
30
- @@override_original_url = false
31
-
32
- def self.signed_in_url(user, original_url, controller) # :nodoc:
33
- if original_url && !@@override_original_url
34
- original_url
35
- else
36
- @@signed_in_url.respond_to?(:call) ? @@signed_in_url.call(user, controller) : @@signed_in_url
5
+ Error = Class.new StandardError
6
+ PasswordExistsError = Class.new QuoVadis::Error
7
+
8
+ def self.accessor(*names)
9
+ names.each do |name|
10
+ define_singleton_method name do |val = nil|
11
+ if !val.nil?
12
+ instance_variable_set :"@#{name}", val
13
+ else
14
+ instance_variable_get :"@#{name}"
15
+ end
16
+ end
37
17
  end
38
18
  end
39
19
 
40
- # Code to run when the user has signed in.
41
- mattr_accessor :signed_in_hook
42
- @@signed_in_hook = nil
43
-
44
- def self.signed_in_hook(user, controller) # :nodoc:
45
- @@signed_in_hook.call(user, controller) if @@signed_in_hook
46
- end
47
-
48
- # Code to run when someone has tried but failed to sign in.
49
- mattr_accessor :failed_sign_in_hook
50
- @@failed_sign_in_hook = nil
51
-
52
- def self.failed_sign_in_hook(controller) # :nodoc:
53
- @@failed_sign_in_hook.call(controller) if @@failed_sign_in_hook
54
- end
55
-
56
- # How long to remember user across browser sessions.
57
- mattr_accessor :remember_for
58
- @@remember_for = 2.weeks
20
+ accessor \
21
+ :password_minimum_length, # integer
22
+ :mask_ips, # true | false
23
+ :cookie_name, # string
24
+ :session_lifetime, # :session | ActiveSupport::Duration | [integer] seconds
25
+ :session_lifetime_extend_to_end_of_day,# true | false
26
+ :session_idle_timeout, # :lifetime | ActiveSupport::Duration | [integer] seconds
27
+ :password_reset_token_lifetime, # ActiveSupport::Duration | [integer] seconds
28
+ :mail_headers, # hash
29
+ :accounts_require_confirmation, # true | false
30
+ :account_confirmation_token_lifetime, # ActiveSupport::Duration | [integer] seconds
31
+ :enqueue_transactional_emails, # true | false
32
+ :app_name, # string
33
+ :two_factor_authentication_mandatory, # true | false
34
+ :mount_point # string
35
+
36
+ class << self
37
+ def configure(&block)
38
+ module_eval &block
39
+ end
59
40
 
60
- # The domain to use for remember-me cookies.
61
- mattr_accessor :cookie_domain
62
- @@cookie_domain = :all
41
+ # name - string class name, e.g. 'User'
42
+ # identifier - attribute symbol, e.g. :username
43
+ def register_model(name, identifier)
44
+ models[name] = identifier
45
+ end
63
46
 
47
+ def find_account_by_identifier_in_params(params)
48
+ identifier = detect_identifier params.keys
49
+ Account.find_by identifier: params[identifier]
50
+ end
64
51
 
65
- # Whether the sign-in process is blocked to the user.
66
- mattr_writer :blocked
67
- @@blocked = false
52
+ # model - string class name, e.g. 'User'
53
+ # returns string humanised name for the model's identifier, e.g. "Username"
54
+ def humanise_identifier(model)
55
+ klass = model.constantize
56
+ klass.human_attribute_name identifier(model)
57
+ end
68
58
 
69
- def self.blocked?(controller) # :nodoc:
70
- @@blocked.respond_to?(:call) ? @@blocked.call(controller) : @@blocked
71
- end
59
+ # Translates the key in the :quo_vadis scope, returning nil if it does not exist.
60
+ # This allows applications to suppress a QuoVadis translation.
61
+ # For example:
62
+ #
63
+ # en:
64
+ # quo_vadis:
65
+ # require_authentication:
66
+ #
67
+ def translate(key, **options)
68
+ I18n.t key, **options.merge(scope: :quo_vadis, raise: true) rescue nil
69
+ end
72
70
 
71
+ def notify(action, params)
72
+ QuoVadis::Mailer.with(params).send(action).deliver_later
73
+ end
73
74
 
74
- #
75
- # Sign out
76
- #
75
+ def deliver(action, params)
76
+ mail = QuoVadis::Mailer.with(params).send(action)
77
+ QuoVadis.enqueue_transactional_emails ?
78
+ mail.deliver_later :
79
+ mail.deliver_now
80
+ end
77
81
 
78
- # The URL to redirect the user to after s/he signs out.
79
- mattr_accessor :signed_out_url
80
- @@signed_out_url = :root
82
+ # model - string class name, e.g. 'User'
83
+ # returns attribute symbol, e.g. :username
84
+ def identifier(model)
85
+ models[model]
86
+ end
81
87
 
82
- def self.signed_out_url(controller) # :nodoc:
83
- @@signed_out_url.respond_to?(:call) ? @@signed_out_url.call(controller) : @@signed_out_url
84
- end
88
+ private
85
89
 
90
+ def models
91
+ @models ||= {}
92
+ end
86
93
 
87
- # Code to run just before the user has signed out.
88
- mattr_accessor :signed_out_hook
89
- @@signed_out_hook = nil
94
+ def detect_identifier(candidates)
95
+ (identifiers.map(&:to_s) & candidates.map(&:to_s)).first
96
+ end
90
97
 
91
- def self.signed_out_hook(user, controller) # :nodoc:
92
- @@signed_out_hook.call(user, controller) if @@signed_out_hook
98
+ def identifiers
99
+ models.values.uniq
100
+ end
93
101
  end
102
+ end
94
103
 
104
+ require_relative 'quo_vadis/defaults'
105
+ require_relative 'quo_vadis/engine'
106
+ require_relative 'quo_vadis/crypt'
107
+ require_relative 'quo_vadis/encrypted_type'
108
+ require_relative 'quo_vadis/hmacable'
109
+ require_relative 'quo_vadis/ip_masking'
110
+ require_relative 'quo_vadis/model'
111
+ require_relative 'quo_vadis/current_request_details'
112
+ require_relative 'quo_vadis/controller'
113
+
114
+ ActiveSupport.on_load(:action_controller) do
115
+ include QuoVadis::Controller
116
+ end
95
117
 
96
- #
97
- # Forgotten-password and activation Mailer
98
- #
99
-
100
- # From whom the forgotten-password email should be sent.
101
- mattr_accessor :from
102
- @@from = 'noreply@example.com'
103
-
104
- # Subject of the forgotten-password email.
105
- mattr_accessor :subject_change_password
106
- @@subject_change_password = 'Change your password.'
107
-
108
- # Subject of the invitation email.
109
- mattr_accessor :subject_invitation
110
- @@subject_invitation = 'Activate your account'
111
-
112
-
113
- #
114
- # Miscellaneous
115
- #
116
-
117
- # Layout for the sign-in view.
118
- mattr_accessor :layout
119
- @@layout = nil
120
-
121
-
122
- # Configure from the initializer.
123
- def self.configure
124
- yield self
125
- end
126
-
118
+ ActiveSupport.on_load(:active_record) do
119
+ include QuoVadis::Model
127
120
  end
121
+