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