orthodox 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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