orthodox 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/authentication/USAGE +14 -0
  3. data/lib/generators/authentication/authentication_generator.rb +214 -0
  4. data/lib/generators/authentication/templates/controllers/concerns/authentication.rb.erb +110 -0
  5. data/lib/generators/authentication/templates/controllers/concerns/two_factor_authentication.rb +40 -0
  6. data/lib/generators/authentication/templates/controllers/password_resets_controller.rb.erb +54 -0
  7. data/lib/generators/authentication/templates/controllers/sessions_controller.rb.erb +36 -0
  8. data/lib/generators/authentication/templates/controllers/tfa_sessions_controller.rb.erb +48 -0
  9. data/lib/generators/authentication/templates/controllers/tfas_controller.rb.erb +38 -0
  10. data/lib/generators/authentication/templates/helpers/otp_credentials_helper.rb +33 -0
  11. data/lib/generators/authentication/templates/javascript/tfa_forms.js +19 -0
  12. data/lib/generators/authentication/templates/models/concerns/authenticateable.rb +37 -0
  13. data/lib/generators/authentication/templates/models/concerns/otpable.rb +26 -0
  14. data/lib/generators/authentication/templates/models/concerns/password_resetable.rb +19 -0
  15. data/lib/generators/authentication/templates/models/otp_credential.rb.erb +133 -0
  16. data/lib/generators/authentication/templates/models/password_reset_token.rb +64 -0
  17. data/lib/generators/authentication/templates/models/session.rb.erb +80 -0
  18. data/lib/generators/authentication/templates/models/tfa_session.rb +77 -0
  19. data/lib/generators/authentication/templates/spec/models/otp_credential_spec.rb +215 -0
  20. data/lib/generators/authentication/templates/spec/models/password_reset_token_spec.rb +146 -0
  21. data/lib/generators/authentication/templates/spec/models/session_spec.rb.erb +45 -0
  22. data/lib/generators/authentication/templates/spec/models/tfa_session_spec.rb.erb +115 -0
  23. data/lib/generators/authentication/templates/spec/support/authentication_helpers.rb +18 -0
  24. data/lib/generators/authentication/templates/spec/support/factory_bot.rb +5 -0
  25. data/lib/generators/authentication/templates/spec/system/authentication_spec.rb.erb +25 -0
  26. data/lib/generators/authentication/templates/spec/system/password_resets_spec.rb.erb +73 -0
  27. data/lib/generators/authentication/templates/spec/system/tfa_authentication_spec.rb.erb +38 -0
  28. data/lib/generators/authentication/templates/views/mailers/password_reset_link.html.slim.erb +7 -0
  29. data/lib/generators/authentication/templates/views/password_resets/edit.html.slim.erb +16 -0
  30. data/lib/generators/authentication/templates/views/password_resets/new.html.slim.erb +12 -0
  31. data/lib/generators/authentication/templates/views/sessions/new.html.slim.erb +21 -0
  32. data/lib/generators/authentication/templates/views/tfa_sessions/new.html.slim.erb +26 -0
  33. data/lib/generators/authentication/templates/views/tfas/show.html.slim.erb +9 -0
  34. data/lib/generators/base_controller/USAGE +8 -0
  35. data/lib/generators/base_controller/base_controller_generator.rb +22 -0
  36. data/lib/generators/base_controller/templates/base_controller.rb.erb +7 -0
  37. data/lib/generators/layout_helper/USAGE +8 -0
  38. data/lib/generators/layout_helper/layout_helper_generator.rb +55 -0
  39. data/lib/orthodox/version.rb +1 -1
  40. metadata +39 -2
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal
2
+
3
+ # Controller for managing sessions for <%= plural_class_name %>.
4
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
5
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
6
+ class <%= plural_class_name %>::SessionsController < <%= plural_class_name %>::BaseController
7
+
8
+ skip_before_action :authenticate_<%= singular_name %>
9
+
10
+ def new
11
+ @<%= singular_name %>_session = <%= class_name %>Session.new(<%= singular_name %>_session_params)
12
+ end
13
+
14
+ def create
15
+ @<%= singular_name %>_session = <%= class_name %>Session.new(<%= singular_name %>_session_params)
16
+ if @<%= singular_name %>_session.valid?
17
+ sign_in(@<%= singular_name %>_session.<%= singular_name %>, as: :<%= singular_name %>)
18
+ redirect_to(<%= plural_name %>_dashboard_url, notice: "Successfully signed in")
19
+ else
20
+ render :new
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ sign_out(:<%= singular_name %>)
26
+ redirect_to root_url, notice: "Successfully signed out"
27
+ end
28
+
29
+ private
30
+
31
+ def <%= singular_name %>_session_params
32
+ return {} unless params.key?(:<%= singular_name %>_session)
33
+ params.require(:<%= singular_name %>_session).permit(:email, :password)
34
+ end
35
+
36
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal
2
+ #
3
+ # Controller for managing two-factor sessions for <%= plural_class_name %>.
4
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
5
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
6
+ class <%= plural_class_name %>::TfaSessionsController < <%= plural_class_name %>::BaseController
7
+
8
+ skip_before_action :authenticate_<%= singular_name %>
9
+
10
+ before_action :authenticate_<%= singular_name %>_without_tfa
11
+
12
+ before_action :ensure_<%= singular_name %>_has_active_tfa
13
+
14
+ before_action :ensure_<%= singular_name %>_not_tfa_authenticated
15
+
16
+ def new
17
+ @tfa_session = TfaSession.new
18
+ end
19
+
20
+ def create
21
+ @tfa_session = TfaSession.new(permitted_params.merge(record: current_<%= singular_name %>))
22
+ if @tfa_session.valid?
23
+ current_<%= singular_name %>.otp_credential.consume_recovery_code!(permitted_params[:recovery_code])
24
+ sign_in(current_<%= singular_name %>, as: :<%= singular_name %>, tfa: true)
25
+ redirect_to <%= singular_name %>_tfa_success_redirect_url
26
+ else
27
+ render :new
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def permitted_params
34
+ params.require(:tfa_session).permit(:otp, :recovery_code)
35
+ end
36
+
37
+ def ensure_<%= singular_name %>_has_active_tfa
38
+ return if current_<%= singular_name %>.tfa?
39
+ redirect_to <%= singular_name %>_tfa_success_redirect_url
40
+ end
41
+
42
+ def ensure_<%= singular_name %>_not_tfa_authenticated
43
+ if current_<%= singular_name %>_tfa_authenticated?
44
+ redirect_to <%= singular_name %>_tfa_success_redirect_url
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal
2
+ #
3
+ # Controller for managing two-factor credentials for <%= plural_class_name %>.
4
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
5
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
6
+ class <%= plural_class_name %>::TfasController < <%= plural_class_name %>::BaseController
7
+
8
+ skip_before_action :authenticate_<%= singular_name %>
9
+
10
+ before_action :authenticate_<%= singular_name %>_without_tfa
11
+
12
+ ##
13
+ # How long will we show the QRCode and recovery codes before they can no longer be
14
+ # accessed?
15
+ CAPTURE_TIME_ALLOWANCE = 15.seconds
16
+
17
+ def create
18
+ current_<%= singular_name %>.create_otp_credential!
19
+ redirect_to(<%= plural_name %>_tfa_url,
20
+ notice: "Successfully activated Two-Factor Authentication")
21
+ end
22
+
23
+ # This is where the <%= singular_name %> gets to see their recovery codes and QR Code.
24
+ # After CAPTURE_TIME_ALLOWANCE they cannot re-visit this page
25
+ def show
26
+ if current_<%= singular_name %>.otp_credential.created_at < CAPTURE_TIME_ALLOWANCE.ago
27
+ redirect_to <%= plural_name %>_dashboard_url
28
+ end
29
+ end
30
+
31
+ def destroy
32
+ current_<%= singular_name %>.destroy_otp_credential
33
+ redirect_to(<%= plural_name %>_dashboard_url,
34
+ notice: "Successfully de-activated Two-Factor Authentication")
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal
2
+
3
+ # Helper for one-time-password authentication methods
4
+ #
5
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
6
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
7
+ module OtpCredentialsHelper
8
+
9
+ require 'rqrcode'
10
+
11
+ # SVG code for a given OtpCredential. Use this to add a QR code to a page
12
+ #
13
+ # otp_credential - An OtpCredential to show the SVG for.
14
+ #
15
+ # Retuns String of valid HTML
16
+ def svg_url_for_otp_credential(otp_credential)
17
+ qrcode = qrcode(otp_credential)
18
+ qrcode.as_svg({
19
+ offset: 0,
20
+ color: '000',
21
+ shape_rendering: 'crispEdges',
22
+ module_size: 3,
23
+ standalone: true
24
+ }).html_safe
25
+ end
26
+
27
+ private
28
+
29
+ def qrcode(otp_credential)
30
+ RQRCode::QRCode.new(otp_credential.url)
31
+ end
32
+
33
+ end
@@ -0,0 +1,19 @@
1
+ // Basic JS functionality for one-time-password form. Designed to be used with jQuery and
2
+ // Bootstrap.
3
+ //
4
+ // Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
5
+ // (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
6
+
7
+ // Initialize method called when jQuery ready
8
+ function init() {
9
+ $("body").on("click", ".js-tfa-link", onClick);
10
+ }
11
+
12
+ // Callback when toggle links are clicked. Shows/hides form fields and links
13
+ function onClick(e){
14
+ e.preventDefault();
15
+ $(".js-tfa-link").toggleClass("d-none");
16
+ $(".js-tfa-field-group").toggleClass("d-none");
17
+ }
18
+
19
+ jQuery(init);
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal
2
+
3
+ # Model concern to provide shared behaviour for authenticating records.
4
+ #
5
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
6
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
7
+ module Authenticateable
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ MINIMUM_PASSWORD_LENGTH = 6
12
+
13
+ MAXIMUM_PASSWORD_LENGTH = 128
14
+
15
+ included do
16
+
17
+ has_secure_password
18
+
19
+ validates :email, email_format: { allow_blank: true }, presence: true
20
+
21
+ validates :password, presence: { if: :validate_presence_of_password? },
22
+ length: { minimum: MINIMUM_PASSWORD_LENGTH,
23
+ maximum: MAXIMUM_PASSWORD_LENGTH,
24
+ allow_blank: true }
25
+
26
+ end
27
+
28
+ private
29
+
30
+ def validate_presence_of_password?
31
+ new_record? || changes.include?("password")
32
+ end
33
+
34
+ module ClassMethods
35
+ end
36
+
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal
2
+ #
3
+ # Model concern to provide shared behaviour for two-factor auth (one-time password)
4
+ #
5
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
6
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
7
+ module Otpable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+
12
+ has_one :otp_credential, as: :authable
13
+
14
+ delegate :valid_otp?, :valid_recovery_code?, to: :otp_credential
15
+
16
+ end
17
+
18
+ def tfa?
19
+ otp_credential.present?
20
+ end
21
+
22
+ def destroy_otp_credential
23
+ otp_credential.destroy
24
+ end
25
+
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal
2
+ #
3
+ # Model concern to provide shared behaviour for password resets
4
+ #
5
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
6
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
7
+ module PasswordResetable
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ has_one :password_reset_token, as: :resetable, dependent: :destroy
13
+ end
14
+
15
+ def destroy_password_reset_token
16
+ password_reset_token.try(:destroy)
17
+ end
18
+
19
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal
2
+ #
3
+ # Model for managing one-time password credentials for a given Member
4
+ #
5
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
6
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
7
+ #
8
+ # == Schema Information
9
+ #
10
+ # Table name: otp_credentials
11
+ #
12
+ # id :bigint not null, primary key
13
+ # authable_type :string not null
14
+ # last_used_at :datetime
15
+ # recovery_codes :json
16
+ # secret :string(32)
17
+ # authable_id :bigint not null
18
+ #
19
+ # Indexes
20
+ #
21
+ # index_otp_credentials_on_authable_type_and_authable_id (authable_type,authable_id)
22
+ #
23
+
24
+ class OtpCredential < ApplicationRecord
25
+
26
+ ##
27
+ # How much of a 'grace' period should we give the user, after which we will accept
28
+ # expired OTPs. Time in seconds.
29
+ DRIFT_ALLOWANCE = 15
30
+
31
+ # ==============
32
+ # = Attributes =
33
+ # ==============
34
+
35
+ attr_readonly :authable_type, :authable_id
36
+
37
+ serialize :recovery_codes
38
+
39
+ # ================
40
+ # = Associations =
41
+ # ================
42
+
43
+ belongs_to :authable, polymorphic: true
44
+
45
+
46
+ # =============
47
+ # = Callbacks =
48
+ # =============
49
+
50
+ before_create :set_secret
51
+
52
+ before_create :set_last_used_at
53
+
54
+ before_create :set_recovery_codes
55
+
56
+
57
+ # ===========================
58
+ # = Public instance methods =
59
+ # ===========================
60
+
61
+ # URL for generating QR code for this OTP.
62
+ #
63
+ # Returns String
64
+ def url
65
+ totp.provisioning_uri(authable.email)
66
+ end
67
+
68
+ # Test the given code against the expected current value.
69
+ #
70
+ # Returns Integer (Timestamp)
71
+ # Returns nil
72
+ def valid_otp?(test_value)
73
+ if result = totp.verify(test_value,
74
+ after: last_used_at,
75
+ drift_behind: DRIFT_ALLOWANCE)
76
+ touch(:last_used_at)
77
+ end
78
+ result
79
+ end
80
+
81
+ # Test the given recovery code against the stored recovery_codes
82
+ #
83
+ # Returns Boolean
84
+ def valid_recovery_code?(test_value)
85
+ test_value.to_s.in?(recovery_codes)
86
+ end
87
+
88
+ # Removes a used recovery code from the recovery_codes list.
89
+ #
90
+ # Returns Boolean
91
+ def consume_recovery_code!(recovery_code)
92
+ array = recovery_codes
93
+ array.delete(recovery_code)
94
+ update_attribute(:recovery_codes, array)
95
+ end
96
+
97
+ private
98
+
99
+ # The expected current OTP value. This shouldn't need to be required in production
100
+ #
101
+ # Returns String
102
+ def current_otp
103
+ totp.now
104
+ end
105
+
106
+ # Set the secret value to a random base 32 String
107
+ #
108
+ # Returns String
109
+ def set_secret
110
+ self.secret = ROTP::Base32.random
111
+ end
112
+
113
+ # Ensure the last_used_at time is always present and a past datetime.
114
+ def set_last_used_at
115
+ self.last_used_at = 5.minutes.ago
116
+ end
117
+
118
+ def set_recovery_codes
119
+ self.recovery_codes = 10.times.map { generate_recovery_code }
120
+ end
121
+
122
+ def generate_recovery_code
123
+ "#{SecureRandom.hex(3)[0..4]}-#{SecureRandom.hex(3)[0..4]}"
124
+ end
125
+
126
+ # An instance of the TOTP to test codes against.
127
+ #
128
+ # Returns ROTP::TOTP
129
+ def totp
130
+ @totp ||= ROTP::TOTP.new(secret, issuer: "<%= Rails.application.class.module_parent.name %>")
131
+ end
132
+
133
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal
2
+
3
+ #
4
+ # Model for managing password reset tokens
5
+ #
6
+ # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
7
+ # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
8
+ #
9
+ # == Schema Information
10
+ #
11
+ # Table name: password_reset_tokens
12
+ #
13
+ # id :bigint not null, primary key
14
+ # expires_at :datetime
15
+ # resetable_type :string not null
16
+ # secret :string
17
+ # created_at :datetime not null
18
+ # updated_at :datetime not null
19
+ # resetable_id :bigint not null
20
+ #
21
+ # Indexes
22
+ #
23
+ # index_password_reset_tokens_on_expires_at (expires_at)
24
+ # index_password_reset_tokens_on_resetable_type_and_resetable_id (resetable_type,resetable_id)
25
+ # index_password_reset_tokens_on_secret (secret) UNIQUE
26
+ #
27
+
28
+ class PasswordResetToken < ApplicationRecord
29
+
30
+ # =============
31
+ # = Constants =
32
+ # =============
33
+
34
+ ##
35
+ # How long should password reset links be valid for?
36
+ EXPIRES_AFTER = 15.minutes
37
+
38
+ # ================
39
+ # = Associations =
40
+ # ================
41
+
42
+ belongs_to :resetable, polymorphic: true
43
+
44
+ # ==============
45
+ # = Attributes =
46
+ # ==============
47
+
48
+ has_secure_token :secret
49
+
50
+ attr_readonly :resetable_type, :resetable_id, :expires_at, :secret
51
+
52
+ before_create :set_expires_at
53
+
54
+ def expired?
55
+ expires_at <= Time.current
56
+ end
57
+
58
+ private
59
+
60
+ def set_expires_at
61
+ self.expires_at = EXPIRES_AFTER.from_now
62
+ end
63
+
64
+ end