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