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