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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f88d03f6e7af9cdf80036cac8d4729085eded3c2fab7397fcf1aa11342614529
4
- data.tar.gz: 3e0cb40bc7890ff8a86e6206ad7d2c9e480aab0913c54e72aa8ff3167f98c912
3
+ metadata.gz: 616ba8695069097dae8c09c0912d366b2491ea1747d683e91fce2ee86bc91456
4
+ data.tar.gz: 05b4176b9a0c81b779ab5ac0490e21437eb3eaec6daad9fa98ce503de0481b4a
5
5
  SHA512:
6
- metadata.gz: b4286e0501d78936b5e0af3ad5d2005a3f00afe97e5bdbb045743aae8c47baf43573f6be896714e4467f90787561c023ce57f200875707042f157ba68f1619ba
7
- data.tar.gz: 4f4d3f93e1f76cb962183b0cda3ec7c9f9e1cff2a0b776b3fcb2c4e292152f54fbb40d34b8aa3e83d591135f63016cef19d1983695747aafdf013b68ba9b33da
6
+ metadata.gz: 436f151edd60aa7a0b8437aa7652d4406f95a78ae9d26aa4b05e0dd5948c86c71baf08225a58162cd50039e62688594ea8a57e433eef4b4459dea42f6b3de0a6
7
+ data.tar.gz: da50fc10b1b96d2a1f6b34d5ae13a57b5c9aa25b2cef9c8f9a8e719d7afa4d25595d6261493c913732fd8da36bc7f546cf9791fffe7a1080943ca975b1d361db
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Adds boilerplate controllers, models, and views for authenticating a model.
3
+
4
+ Example:
5
+ rails generate authentication Member
6
+
7
+ This will create:
8
+ app/models/concerns/authenticateable.rb
9
+ app/controllers/concerns/authentication.rb
10
+ app/controllers/members/sessions_controller.rb
11
+ app/models/member_session.rb
12
+ app/views/members/sessions/new.html.slim
13
+ spec/system/members/authentication_spec.rb
14
+ spec/models/member_session_spec.rb
@@ -0,0 +1,214 @@
1
+ class AuthenticationGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ desc "Creates authentication views, controllers and models for a given Model"
5
+
6
+ class_option :skip_views, type: :boolean, default: false
7
+
8
+ class_option :skip_tests, type: :boolean, default: false
9
+
10
+ class_option :two_factor, type: :boolean, default: false
11
+
12
+ def create_controllers
13
+ generate "base_controller", class_name
14
+ template "controllers/sessions_controller.rb.erb",
15
+ "app/controllers/#{plural_file_name}/sessions_controller.rb"
16
+ template "controllers/password_resets_controller.rb.erb",
17
+ "app/controllers/#{plural_file_name}/password_resets_controller.rb"
18
+
19
+ if options[:two_factor]
20
+ template "controllers/tfa_sessions_controller.rb.erb",
21
+ "app/controllers/#{plural_file_name}/tfa_sessions_controller.rb"
22
+
23
+ template "controllers/tfas_controller.rb.erb",
24
+ "app/controllers/#{plural_file_name}/tfas_controller.rb"
25
+ end
26
+ end
27
+
28
+ def extend_controllers
29
+ inject_into_class "app/controllers/#{plural_name}/base_controller.rb",
30
+ "#{plural_class_name}::BaseController",
31
+ " authenticate_model :#{singular_name}, tfa: #{options[:two_factor]}\n"
32
+ include_module_in_controller("#{plural_name}/base_controller", "Authentication")
33
+
34
+ end
35
+
36
+ def ensure_helpers
37
+ if options[:two_factor]
38
+ copy_file "helpers/otp_credentials_helper.rb", "app/helpers/otp_credentials_helper.rb"
39
+ end
40
+ end
41
+
42
+ def ensure_js
43
+ if options[:two_factor]
44
+ copy_file "javascript/tfa_forms.js", "app/javascript/packs/tfa_forms.js"
45
+ end
46
+ end
47
+
48
+ def create_mailer
49
+ generate "mailer", "#{class_name}Mailer"
50
+ inject_into_class "app/mailers/#{singular_name}_mailer.rb", "#{class_name}Mailer", <<~RUBY
51
+
52
+ def password_reset_link(#{singular_name})
53
+ @#{singular_name} = #{singular_name}
54
+ mail(to: #{singular_name}.email, subject: "Reset your password")
55
+ end
56
+
57
+ RUBY
58
+ template "views/mailers/password_reset_link.html.slim.erb",
59
+ "app/views/#{singular_name}_mailer/password_reset_link.html.slim"
60
+ end
61
+
62
+ def create_models
63
+ copy_file "models/password_reset_token.rb", "app/models/password_reset_token.rb"
64
+ if options[:two_factor]
65
+ template "models/otp_credential.rb.erb", "app/models/otp_credential.rb"
66
+ end
67
+ end
68
+
69
+ def extend_models
70
+ include_module_in_model(class_name, "Authenticateable")
71
+ include_module_in_model(class_name, "PasswordResetable")
72
+ if options[:two_factor]
73
+ include_module_in_model(class_name, "Otpable")
74
+ end
75
+ end
76
+
77
+ def create_form_objects
78
+ template "models/session.rb.erb", "app/models/#{singular_name}_session.rb"
79
+ if options[:two_factor]
80
+ copy_file "models/tfa_session.rb", "app/models/tfa_session.rb"
81
+ end
82
+ end
83
+
84
+ def ensure_concerns
85
+ template "controllers/concerns/authentication.rb.erb",
86
+ "app/controllers/concerns/authentication.rb"
87
+ copy_file "models/concerns/authenticateable.rb",
88
+ "app/models/concerns/authenticateable.rb"
89
+ copy_file "models/concerns/password_resetable.rb",
90
+ "app/models/concerns/password_resetable.rb"
91
+
92
+ if options[:two_factor]
93
+ copy_file "controllers/concerns/two_factor_authentication.rb",
94
+ "app/controllers/concerns/two_factor_authentication.rb"
95
+
96
+ copy_file "models/concerns/otpable.rb", "app/models/concerns/otpable.rb"
97
+ end
98
+ end
99
+
100
+ def create_migrations
101
+ generate "migration", "create_password_reset_tokens secret:token "\
102
+ "expires_at:datetime:index "\
103
+ "resetable:references{polymorphic}"
104
+ if options[:two_factor]
105
+ generate "migration", "create_otp_credentials \
106
+ created_at:datetime \
107
+ last_used_at:datetime \
108
+ secret:string{32} \
109
+ authable:references{polymorphic} \
110
+ recovery_codes:json"
111
+ end
112
+ end
113
+
114
+ def create_view_templates
115
+ return if options[:skip_views]
116
+ template "views/sessions/new.html.slim.erb",
117
+ "app/views/#{plural_name}/sessions/new.html.slim"
118
+
119
+ template "views/password_resets/new.html.slim.erb",
120
+ "app/views/#{plural_name}/password_resets/new.html.slim"
121
+
122
+ template "views/password_resets/edit.html.slim.erb",
123
+ "app/views/#{plural_name}/password_resets/edit.html.slim"
124
+
125
+ if options[:two_factor]
126
+ template "views/tfa_sessions/new.html.slim.erb",
127
+ "app/views/#{plural_name}/tfa_sessions/new.html.slim"
128
+ template "views/tfas/show.html.slim.erb",
129
+ "app/views/#{plural_name}/tfas/show.html.slim"
130
+
131
+ end
132
+ end
133
+
134
+ def create_specs
135
+ return if options[:skip_tests]
136
+
137
+ copy_file "spec/support/factory_bot.rb", "spec/support/factory_bot.rb"
138
+
139
+ template "spec/system/authentication_spec.rb.erb",
140
+ "spec/system/#{plural_name}/authentication_spec.rb"
141
+
142
+ template "spec/system/password_resets_spec.rb.erb",
143
+ "spec/system/#{plural_name}/password_resets_spec.rb"
144
+
145
+ template "spec/models/session_spec.rb.erb",
146
+ "spec/models/#{singular_name}_session_spec.rb"
147
+
148
+ copy_file "spec/models/password_reset_token_spec.rb",
149
+ "spec/models/password_reset_token_spec.rb"
150
+
151
+ if options[:two_factor]
152
+ copy_file "spec/support/authentication_helpers.rb",
153
+ "spec/support/authentication_helpers.rb"
154
+ template "spec/models/tfa_session_spec.rb.erb", "spec/models/tfa_session_spec.rb"
155
+ copy_file "spec/models/otp_credential_spec.rb",
156
+ "spec/models/otp_credential_spec.rb"
157
+ template "spec/system/tfa_authentication_spec.rb.erb",
158
+ "spec/system/#{plural_name}/tfa_authentication_spec.rb"
159
+
160
+ end
161
+ end
162
+
163
+ def create_factories
164
+ generate "factory_bot:model", "otp_credential"
165
+ generate "factory_bot:model", "password_reset_token"
166
+ generate "factory_bot:model", "#{singular_name}_session email:email password:password"
167
+ end
168
+
169
+ def ensure_gems
170
+ gem "validates_email_format_of", version: "~> 1.6"
171
+ gem "slim-rails"
172
+ gem "factory_bot_rails"
173
+ gem "faker"
174
+ if options[:two_factor]
175
+ gem "rotp", version: "~> 5.1.0"
176
+ gem "rqrcode", require: false
177
+ end
178
+ end
179
+
180
+ def create_routes
181
+ route <<~RUBY
182
+ namespace :#{plural_name} do
183
+
184
+ resource :session, only: [:new, :create, :destroy]
185
+
186
+ #{"resource :tfa_session, only: [:new, :create]" if options[:two_factor]}
187
+
188
+ resource :tfa, only: [:create, :show, :destroy]
189
+
190
+ resources :password_resets, only: [:new, :create, :edit, :update], param: :token
191
+
192
+ end
193
+ RUBY
194
+ end
195
+
196
+ private
197
+
198
+ def plural_class_name
199
+ class_name.pluralize
200
+ end
201
+
202
+ def include_module_in_controller(controller_name, module_name)
203
+ class_name = controller_name.classify
204
+ inject_into_class "app/controllers/#{controller_name}.rb", class_name,
205
+ " include #{module_name}\n"
206
+
207
+ end
208
+
209
+ def include_module_in_model(model_name, module_name)
210
+ inject_into_class "app/models/#{model_name.underscore}.rb", model_name,
211
+ " include #{module_name}\n"
212
+
213
+ end
214
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal
2
+
3
+ # Concern added to controllres to provide methods for authentication.
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 Authentication
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ private
12
+
13
+ # Sign in a given record as a given type
14
+ #
15
+ # record - An AppliationRecord subclass instance (e.g. A Member)
16
+ # as - A String or Symbol with the model name (e.g. "member")
17
+ <%- if options[:two_factor] -%>
18
+ # tfa - Mark as Two-Factor authentication authenticated.
19
+ <%- end -%>
20
+ #
21
+ # Returns Integer
22
+ def sign_in(record, as:<%= ", tfa: false" if options[:two_factor] -%>)
23
+ session[:"#{as}_id"] = record.id.to_i
24
+ <%- if options[:two_factor] -%>
25
+ session[:"#{as}_tfa_authenticated"] = tfa
26
+ <%- end -%>
27
+ end
28
+
29
+ # Sign out a given record type.
30
+ #
31
+ # as - A String or Symbol with the model name (e.g. "member")
32
+ def sign_out(as)
33
+ session[:"#{as}_id"] = nil
34
+ <%- if options[:two_factor] -%>
35
+ session[:"#{as}_tfa_authenticated"] = nil
36
+ <%- end -%>
37
+ instance_variable_set("@current_#{as}", nil)
38
+ end
39
+
40
+ module ClassMethods
41
+
42
+ # Create a bunch of authentication methods for a given ActiveRecord model
43
+ def authenticate_model(model_name, **options)
44
+ # Define a current_<model> method to load the currently signed in record, if present
45
+ #
46
+ # Returns ApplicationRecord subclass
47
+ define_method(:"current_#{model_name}") do
48
+ instance_method_name = "@current_#{model_name}"
49
+ if instance_variable_get(instance_method_name).present?
50
+ instance_variable_get(instance_method_name)
51
+ else
52
+ scope = send(:"#{model_name}_auth_scope")
53
+ instance_variable_set(instance_method_name,
54
+ scope.find_by(id: session[:"#{model_name}_id"]))
55
+ end
56
+ end
57
+
58
+ # Define a controller before_action method to authenticate a record for the given
59
+ # model. Redirects to the <model_name>_failed_authentication_url if not passed.
60
+ #
61
+ # Returns nil
62
+ define_method(:"authenticate_#{model_name}") do
63
+ unless send(:"current_#{model_name}?")
64
+ redirect_to send(:"#{model_name}_failed_authentication_url"),
65
+ warn: "You must be signed in to do that"
66
+ end
67
+ end
68
+
69
+ before_action :"authenticate_#{model_name}"
70
+
71
+ # Creates a scope that records are loaded through when being authenticated. Subclass
72
+ # this method to customise the load conditions.
73
+ #
74
+ # Returns ActiveRecord::Relation
75
+ define_method(:"#{model_name}_auth_scope") do
76
+ model_name.to_s.classify.constantize.all
77
+ end
78
+
79
+ # Creates a boolean method to check if a current_<model_name> has been authenticated
80
+ # or not.
81
+ #
82
+ # Returns Boolean
83
+ define_method(:"current_#{model_name}?") { send(:"current_#{model_name}").present? }
84
+
85
+ alias_method :"#{model_name}_signed_in?", :"current_#{model_name}?"
86
+
87
+ alias_method :"#{model_name}_failed_authentication_url",
88
+ :"new_#{model_name.to_s.pluralize}_session_url"
89
+
90
+ helper_method :"current_#{model_name}"
91
+ helper_method :"current_#{model_name}?"
92
+ helper_method :"#{model_name}_signed_in?"
93
+
94
+ private :"current_#{model_name}"
95
+ private :"current_#{model_name}?"
96
+ private :"#{model_name}_signed_in?"
97
+ private :"authenticate_#{model_name}"
98
+
99
+ <%- if options[:two_factor] -%>
100
+ # This is included if the authentication generator is run with --two-factor=true
101
+ if options[:tfa] == true
102
+ include TwoFactorAuthentication
103
+ define_tfa_methods(model_name)
104
+ end
105
+ <%- end -%>
106
+ end
107
+
108
+ end
109
+
110
+ end
@@ -0,0 +1,40 @@
1
+ module TwoFactorAuthentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ def define_tfa_methods(model_name)
11
+ define_method :"authenticate_#{model_name}_with_tfa" do
12
+
13
+ send(:"authenticate_#{model_name}_without_tfa")
14
+
15
+ record = send(:"current_#{model_name}")
16
+ return unless record
17
+ if record.tfa? && !send(:"current_#{model_name}_tfa_authenticated?")
18
+ redirect_to send(:"new_#{model_name.to_s.pluralize}_tfa_session_url"),
19
+ warn: "You cannot proceed without authenticating"
20
+ end
21
+
22
+ end
23
+
24
+ define_method :"current_#{model_name}_tfa_authenticated?" do
25
+ session[:"#{model_name}_tfa_authenticated"] == true
26
+ end
27
+
28
+ define_method :"#{model_name}_tfa_success_redirect_url" do
29
+ send(:"#{model_name.to_s.pluralize}_dashboard_url")
30
+ end
31
+
32
+ alias_method :"authenticate_#{model_name}_without_tfa", :"authenticate_#{model_name}"
33
+
34
+ alias_method :"authenticate_#{model_name}", :"authenticate_#{model_name}_with_tfa"
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal
2
+ class <%= plural_class_name %>::PasswordResetsController < <%= plural_class_name %>::BaseController
3
+
4
+ skip_before_action :authenticate_<%= singular_name %>
5
+
6
+ def new
7
+ end
8
+
9
+ def create
10
+ @<%= singular_name %> = <%= plural_name %>_scope.find_by(email: permitted_params[:email])
11
+ if @<%= singular_name %>
12
+ @<%= singular_name %>.create_password_reset_token
13
+ <%= class_name %>Mailer.password_reset_link(@<%= singular_name %>).deliver_later
14
+ end
15
+ redirect_to new_<%= plural_name %>_session_url,
16
+ notice: "Please check your email for a password reset link"
17
+ end
18
+
19
+ def edit
20
+ @<%= singular_name %> = find_<%= singular_name %>_from_token(params[:token])
21
+ if @<%= singular_name %>.nil? or @<%= singular_name %>.password_reset_token.expired?
22
+ redirect_to new_<%= plural_name %>_session_url,
23
+ error: "The link you followed does not look valid"
24
+ end
25
+ end
26
+
27
+ def update
28
+ @<%= singular_name %> = find_<%= singular_name %>_from_token(params[:token])
29
+ if @<%= singular_name %>.update(password: permitted_params[:password],
30
+ password_confirmation: permitted_params[:password_confirmation])
31
+ @<%= singular_name %>.destroy_password_reset_token
32
+ redirect_to new_<%= plural_name %>_session_url, notice: "Successfully reset your password"
33
+ else
34
+ render :edit
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def permitted_params
41
+ params.require(:<%= singular_name %>).permit(:email, :password, :password_confirmation)
42
+ end
43
+
44
+ # Change me to suit the scoping requirements for this project
45
+ def <%= plural_name %>_scope
46
+ <%= class_name %>.all
47
+ end
48
+
49
+ def find_<%= singular_name %>_from_token(token)
50
+ <%= plural_name %>_scope.joins(:password_reset_token)
51
+ .where(password_reset_tokens: { secret: params[:token] }).first
52
+ end
53
+
54
+ end