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.
- checksums.yaml +4 -4
- data/lib/generators/authentication/USAGE +14 -0
- data/lib/generators/authentication/authentication_generator.rb +214 -0
- data/lib/generators/authentication/templates/controllers/concerns/authentication.rb.erb +110 -0
- data/lib/generators/authentication/templates/controllers/concerns/two_factor_authentication.rb +40 -0
- data/lib/generators/authentication/templates/controllers/password_resets_controller.rb.erb +54 -0
- data/lib/generators/authentication/templates/controllers/sessions_controller.rb.erb +36 -0
- data/lib/generators/authentication/templates/controllers/tfa_sessions_controller.rb.erb +48 -0
- data/lib/generators/authentication/templates/controllers/tfas_controller.rb.erb +38 -0
- data/lib/generators/authentication/templates/helpers/otp_credentials_helper.rb +33 -0
- data/lib/generators/authentication/templates/javascript/tfa_forms.js +19 -0
- data/lib/generators/authentication/templates/models/concerns/authenticateable.rb +37 -0
- data/lib/generators/authentication/templates/models/concerns/otpable.rb +26 -0
- data/lib/generators/authentication/templates/models/concerns/password_resetable.rb +19 -0
- data/lib/generators/authentication/templates/models/otp_credential.rb.erb +133 -0
- data/lib/generators/authentication/templates/models/password_reset_token.rb +64 -0
- data/lib/generators/authentication/templates/models/session.rb.erb +80 -0
- data/lib/generators/authentication/templates/models/tfa_session.rb +77 -0
- data/lib/generators/authentication/templates/spec/models/otp_credential_spec.rb +215 -0
- data/lib/generators/authentication/templates/spec/models/password_reset_token_spec.rb +146 -0
- data/lib/generators/authentication/templates/spec/models/session_spec.rb.erb +45 -0
- data/lib/generators/authentication/templates/spec/models/tfa_session_spec.rb.erb +115 -0
- data/lib/generators/authentication/templates/spec/support/authentication_helpers.rb +18 -0
- data/lib/generators/authentication/templates/spec/support/factory_bot.rb +5 -0
- data/lib/generators/authentication/templates/spec/system/authentication_spec.rb.erb +25 -0
- data/lib/generators/authentication/templates/spec/system/password_resets_spec.rb.erb +73 -0
- data/lib/generators/authentication/templates/spec/system/tfa_authentication_spec.rb.erb +38 -0
- data/lib/generators/authentication/templates/views/mailers/password_reset_link.html.slim.erb +7 -0
- data/lib/generators/authentication/templates/views/password_resets/edit.html.slim.erb +16 -0
- data/lib/generators/authentication/templates/views/password_resets/new.html.slim.erb +12 -0
- data/lib/generators/authentication/templates/views/sessions/new.html.slim.erb +21 -0
- data/lib/generators/authentication/templates/views/tfa_sessions/new.html.slim.erb +26 -0
- data/lib/generators/authentication/templates/views/tfas/show.html.slim.erb +9 -0
- data/lib/generators/base_controller/USAGE +8 -0
- data/lib/generators/base_controller/base_controller_generator.rb +22 -0
- data/lib/generators/base_controller/templates/base_controller.rb.erb +7 -0
- data/lib/generators/layout_helper/USAGE +8 -0
- data/lib/generators/layout_helper/layout_helper_generator.rb +55 -0
- data/lib/orthodox/version.rb +1 -1
- metadata +39 -2
@@ -0,0 +1,45 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
# Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
|
4
|
+
# (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
|
5
|
+
RSpec.describe <%= class_name %>Session, type: :model do
|
6
|
+
|
7
|
+
context "when credentials valid" do
|
8
|
+
|
9
|
+
let(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
subject.email = <%= singular_name %>.email
|
13
|
+
subject.password = <%= singular_name %>.password
|
14
|
+
end
|
15
|
+
|
16
|
+
it { is_expected.to be_valid }
|
17
|
+
|
18
|
+
it "sets the <%= singular_name %> attribute to the matching record" do
|
19
|
+
subject.valid?
|
20
|
+
expect(subject.<%= singular_name %>).to eql(<%= singular_name %>)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when credentials are invalid" do
|
26
|
+
|
27
|
+
before do
|
28
|
+
subject.email = Faker::Internet.safe_email
|
29
|
+
subject.password = "wrong-password"
|
30
|
+
end
|
31
|
+
|
32
|
+
it { is_expected.not_to be_valid }
|
33
|
+
|
34
|
+
it "sets the <%= singular_name %> attribute to nil" do
|
35
|
+
expect(subject.<%= singular_name %>).to be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "has errors on base" do
|
39
|
+
subject.valid?
|
40
|
+
expect(subject.errors[:base]).to be_present
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
# Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
|
4
|
+
# (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
|
5
|
+
RSpec.describe TfaSession, type: :model do
|
6
|
+
|
7
|
+
let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
8
|
+
|
9
|
+
before do
|
10
|
+
<%= singular_name %>.create_otp_credential!
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#valid?" do
|
14
|
+
|
15
|
+
subject { TfaSession.new(record: <%= singular_name %>) }
|
16
|
+
|
17
|
+
context "when otp correct" do
|
18
|
+
|
19
|
+
before do
|
20
|
+
subject.otp = <%= singular_name %>.otp_credential.send(:current_otp)
|
21
|
+
end
|
22
|
+
|
23
|
+
it { is_expected.to be_valid }
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when otp incorrect" do
|
28
|
+
|
29
|
+
before do
|
30
|
+
subject.otp = "12345"
|
31
|
+
end
|
32
|
+
|
33
|
+
it { is_expected.to be_invalid }
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when recovery code correct" do
|
38
|
+
|
39
|
+
before do
|
40
|
+
subject.recovery_code = <%= singular_name %>.otp_credential.recovery_codes.sample
|
41
|
+
end
|
42
|
+
|
43
|
+
it { is_expected.to be_valid }
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when recovery code incorrect" do
|
48
|
+
|
49
|
+
before do
|
50
|
+
subject.recovery_code = "abcd-efgh"
|
51
|
+
end
|
52
|
+
|
53
|
+
it { is_expected.to be_invalid }
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
describe "#otp?" do
|
60
|
+
|
61
|
+
context "when present" do
|
62
|
+
|
63
|
+
before do
|
64
|
+
subject.otp = "12345"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "is expected to be truthy" do
|
68
|
+
expect(subject.otp?).to be_truthy
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when blank" do
|
74
|
+
|
75
|
+
before do
|
76
|
+
subject.otp = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it "is expected to be falsey" do
|
80
|
+
expect(subject.otp?).to be_falsey
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#recovery_code?" do
|
88
|
+
|
89
|
+
context "when present" do
|
90
|
+
|
91
|
+
before do
|
92
|
+
subject.recovery_code = "abcd-defg"
|
93
|
+
end
|
94
|
+
|
95
|
+
it "is expected to be truthy" do
|
96
|
+
expect(subject.recovery_code?).to be_truthy
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
context "when blank" do
|
102
|
+
|
103
|
+
before do
|
104
|
+
subject.recovery_code = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
it "is expected to be falsey" do
|
108
|
+
expect(subject.recovery_code?).to be_falsey
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal
|
2
|
+
#
|
3
|
+
# Helpers for automating sign-in and out during tests
|
4
|
+
# Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
|
5
|
+
# (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
|
6
|
+
#
|
7
|
+
module AuthenticationHelpers
|
8
|
+
def sign_in(record, **options)
|
9
|
+
type = record.class.model_name.singular
|
10
|
+
visit(public_send("new_#{type.pluralize}_session_path"))
|
11
|
+
fill_in :"#{type}_session_email", with: record.email
|
12
|
+
fill_in :"#{type}_session_password", with: record.password
|
13
|
+
click_button "Sign in"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.include(AuthenticationHelpers, type: :system)
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
# Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
|
4
|
+
# (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
|
5
|
+
RSpec.describe "<%= plural_class_name %>::Authentication", type: :system do
|
6
|
+
|
7
|
+
let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
8
|
+
|
9
|
+
scenario "A <%= singular_name %> signs in successfully" do
|
10
|
+
visit new_<%= plural_name %>_session_path
|
11
|
+
fill_in :<%= singular_name %>_session_email, with: <%= singular_name %>.email
|
12
|
+
fill_in :<%= singular_name %>_session_password, with: <%= singular_name %>.password
|
13
|
+
click_button "Sign in"
|
14
|
+
expect(current_path).to eql(<%= plural_name %>_dashboard_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
scenario "A <%= singular_name %> attempts to sign in unsuccessfully" do
|
18
|
+
visit new_<%= plural_name %>_session_path
|
19
|
+
fill_in :<%= singular_name %>_session_email, with: <%= singular_name %>.email
|
20
|
+
fill_in :<%= singular_name %>_session_password, with: "WRONG-PASSWORD"
|
21
|
+
click_button "Sign in"
|
22
|
+
expect(page).to have_text(<%= class_name%>Session::INVALID_CREDENTIALS)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
# Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
|
4
|
+
# (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
|
5
|
+
RSpec.describe "<%= plural_class_name %>::PasswordResets", type: :system do
|
6
|
+
|
7
|
+
include ActiveSupport::Testing::TimeHelpers
|
8
|
+
|
9
|
+
let(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
10
|
+
|
11
|
+
scenario "<%= class_name %> resets password successfully" do
|
12
|
+
visit new_<%= plural_name %>_session_path
|
13
|
+
click_link "Forgot password"
|
14
|
+
fill_in :<%= singular_name %>_email, with: <%= singular_name %>.email
|
15
|
+
click_button "Send"
|
16
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
17
|
+
expect(enqueued_jobs)
|
18
|
+
expect(last_mailer_job[:args][0..1]).to eql(["<%= class_name %>Mailer", "password_reset_link"])
|
19
|
+
visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
|
20
|
+
fill_in(:<%= singular_name %>_password, with: "new-password")
|
21
|
+
fill_in(:<%= singular_name %>_password_confirmation, with: "new-password")
|
22
|
+
click_button("Save")
|
23
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
24
|
+
expect(<%= singular_name %>.reload.authenticate("new-password")).to eql(<%= singular_name %>)
|
25
|
+
end
|
26
|
+
|
27
|
+
scenario "<%= class_name %> clicks link once already used" do
|
28
|
+
visit new_<%= plural_name %>_session_path
|
29
|
+
click_link "Forgot password"
|
30
|
+
fill_in :<%= singular_name %>_email, with: <%= singular_name %>.email
|
31
|
+
click_button "Send"
|
32
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
33
|
+
expect(enqueued_jobs)
|
34
|
+
expect(last_mailer_job[:args][0..1]).to eql(["<%= class_name %>Mailer", "password_reset_link"])
|
35
|
+
visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
|
36
|
+
fill_in(:<%= singular_name %>_password, with: "new-password")
|
37
|
+
fill_in(:<%= singular_name %>_password_confirmation, with: "new-password")
|
38
|
+
click_button("Save")
|
39
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
40
|
+
visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
|
41
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
scenario "<%= class_name %> clicks link once expired" do
|
45
|
+
visit new_<%= plural_name %>_session_path
|
46
|
+
click_link "Forgot password"
|
47
|
+
fill_in :<%= singular_name %>_email, with: <%= singular_name %>.email
|
48
|
+
click_button "Send"
|
49
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
50
|
+
expect(enqueued_jobs)
|
51
|
+
expect(last_mailer_job[:args][0..1]).to eql(["<%= class_name %>Mailer", "password_reset_link"])
|
52
|
+
travel(16.minutes) do
|
53
|
+
visit(edit_<%= plural_name %>_password_reset_path(token: <%= singular_name %>.password_reset_token.secret))
|
54
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
scenario "<%= class_name %> email address not recognised" do
|
59
|
+
visit new_<%= plural_name %>_session_path
|
60
|
+
click_link "Forgot password"
|
61
|
+
fill_in :<%= singular_name %>_email, with: "wrong-email@example.com"
|
62
|
+
click_button "Send"
|
63
|
+
expect(current_path).to eql(new_<%= plural_name %>_session_path)
|
64
|
+
expect(last_mailer_job).to be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def last_mailer_job
|
70
|
+
enqueued_jobs.select { |job| job[:job] == ActionMailer::MailDeliveryJob }.last
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
# Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
|
4
|
+
# (c) Copyright 2019 Katana Code Ltd. All Rights Reserved.
|
5
|
+
RSpec.describe "<%= plural_class_name %>::TfaAuthentication", type: :system do
|
6
|
+
|
7
|
+
let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
|
8
|
+
|
9
|
+
before do
|
10
|
+
<%= singular_name %>.create_otp_credential!
|
11
|
+
end
|
12
|
+
|
13
|
+
scenario "A <%= singular_name %> signs in successfully with otp" do
|
14
|
+
sign_in(<%= singular_name %>, tfa: false)
|
15
|
+
expect(current_path).to eql(new_<%= plural_name %>_tfa_session_path)
|
16
|
+
fill_in(:tfa_session_otp, with: <%= singular_name %>.otp_credential.send(:current_otp))
|
17
|
+
click_button("Submit code")
|
18
|
+
expect(current_path).to eql(<%= plural_name %>_dashboard_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
scenario "A <%= singular_name %> attempts with the wrong otp" do
|
22
|
+
sign_in(<%= singular_name %>, tfa: false)
|
23
|
+
expect(current_path).to eql(new_<%= plural_name %>_tfa_session_path)
|
24
|
+
fill_in(:tfa_session_otp, with: '999999')
|
25
|
+
click_button("Submit code")
|
26
|
+
expect(page).to have_text("not correct")
|
27
|
+
end
|
28
|
+
|
29
|
+
scenario "A <%= singular_name %> signs in successfully with a recovery code" do
|
30
|
+
sign_in(<%= singular_name %>, tfa: false)
|
31
|
+
expect(current_path).to eql(new_<%= plural_name %>_tfa_session_path)
|
32
|
+
click_link("Use a recovery code")
|
33
|
+
fill_in(:tfa_session_recovery_code, with: <%= singular_name %>.otp_credential.recovery_codes.sample)
|
34
|
+
click_button("Submit code")
|
35
|
+
expect(current_path).to eql(<%= plural_name %>_dashboard_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
p Reset your password by clicking on this link
|
2
|
+
|
3
|
+
= link_to(edit_<%= plural_name %>_password_reset_url(token: @<%= singular_name%>.password_reset_token.secret), edit_<%= plural_name %>_password_reset_url(token: @<%= singular_name%>.password_reset_token.secret))
|
4
|
+
|
5
|
+
p
|
6
|
+
em
|
7
|
+
Note, this link will expire after #{@<%= singular_name%>.password_reset_token.expires_at.to_s(:long)}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
- title "Set a new password"
|
2
|
+
|
3
|
+
h1 = title
|
4
|
+
|
5
|
+
= form_tag(<%= plural_name %>_password_reset_path(token: params[:token]), class: "form", method: :patch) do |f|
|
6
|
+
|
7
|
+
.form-group
|
8
|
+
= label(:<%= singular_name %>, :password, "Choose a new password")
|
9
|
+
= password_field(:<%= singular_name %>, :password, class: "form-control")
|
10
|
+
|
11
|
+
.form-group
|
12
|
+
= label(:<%= singular_name %>, :password_confirmation, "Re-enter your password")
|
13
|
+
= password_field(:<%= singular_name %>, :password_confirmation, class: "form-control")
|
14
|
+
|
15
|
+
= submit_tag("Save changes", class: "btn btn-primary")
|
16
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
- title "Request a password reset"
|
2
|
+
|
3
|
+
h1 = title
|
4
|
+
|
5
|
+
= form_tag(<%= plural_name %>_password_resets_path, class: "form") do |f|
|
6
|
+
|
7
|
+
.form-group
|
8
|
+
= label(:<%= singular_name %>, :email, "Email address")
|
9
|
+
= email_field(:<%= singular_name %>, :email, class: "form-control")
|
10
|
+
|
11
|
+
= submit_tag("Send me a link", class: "btn btn-primary")
|
12
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
- title "Sign in"
|
2
|
+
|
3
|
+
= form_for(@<%= singular_name %>_session, url: <%= plural_name %>_session_path) do |f|
|
4
|
+
|
5
|
+
ul
|
6
|
+
- @<%= singular_name %>_session.errors.full_messages.each do |error_message|
|
7
|
+
li = error_message
|
8
|
+
|
9
|
+
.form-group
|
10
|
+
= f.label(:email, "Email")
|
11
|
+
= f.email_field(:email, class: "form-control")
|
12
|
+
|
13
|
+
.form-group
|
14
|
+
= f.label(:password, "Password")
|
15
|
+
= f.password_field(:password, class: "form-control")
|
16
|
+
|
17
|
+
div
|
18
|
+
= f.submit("Sign in")
|
19
|
+
|
20
|
+
div
|
21
|
+
= link_to("Forgot password?", new_<%= plural_name %>_password_reset_path)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
- title "Two-factor authentication"
|
2
|
+
|
3
|
+
= form_for(@tfa_session, url: <%= plural_name %>_tfa_session_path) do |f|
|
4
|
+
|
5
|
+
ul
|
6
|
+
- @tfa_session.errors.full_messages.each do |error_message|
|
7
|
+
li = error_message
|
8
|
+
|
9
|
+
.form-group.js-tfa-field-group
|
10
|
+
= f.label(:otp, "Enter your one-time password")
|
11
|
+
= f.text_field(:otp, class: "form-control", autofocus: true, pattern: "[0-9]{3,6}")
|
12
|
+
|
13
|
+
.form-group.js-tfa-field-group.d-none
|
14
|
+
= f.label(:recovery_code, "Use a recovery code")
|
15
|
+
= f.text_field(:recovery_code, class: "form-control", pattern: "[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}")
|
16
|
+
|
17
|
+
div
|
18
|
+
= link_to("Use a recovery code instead", "#",
|
19
|
+
class: "js-tfa-link js-tfa-link--recovery-code")
|
20
|
+
|
21
|
+
= link_to("Use one-time password instead", "#",
|
22
|
+
class: "js-tfa-link js-tfa-link--otp d-none")
|
23
|
+
|
24
|
+
div
|
25
|
+
= f.submit("Submit code")
|
26
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal
|
2
|
+
|
3
|
+
class BaseControllerGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
|
6
|
+
check_class_collision suffix: "Controller"
|
7
|
+
|
8
|
+
desc "This generator creates a base controller for the named namespace"
|
9
|
+
|
10
|
+
def ensure_file
|
11
|
+
template "base_controller.rb.erb",
|
12
|
+
File.join("app", "controllers", plural_file_name, "base_controller.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
|
18
|
+
def namespace_module
|
19
|
+
file_name.to_s.split("/").first.classify.pluralize
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|