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
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