summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/node_action.rb7
-rw-r--r--app/models/user.rb71
2 files changed, 78 insertions, 0 deletions
diff --git a/app/models/node_action.rb b/app/models/node_action.rb
index afa2195c..a9bb0e45 100644
--- a/app/models/node_action.rb
+++ b/app/models/node_action.rb
@@ -98,6 +98,13 @@ class NodeAction < ApplicationRecord
98 # "detached_from" -- array of unique_names, only when any 98 # "detached_from" -- array of unique_names, only when any
99 # "headline_removed_from" -- array of unique_names, only when any 99 # "headline_removed_from" -- array of unique_names, only when any
100 # 100 #
101 # "otp_enroll" / "otp_disable" / "otp_reset" (second-factor lifecycle;
102 # node column nil; participants: the affected User -- the table's first
103 # User-typed subject. otp_disable is self-service; otp_reset is an
104 # administrator clearing someone else's factor, where actor and
105 # participant differ):
106 # "target_login" -- flat string, the affected account's login
107 #
101 # Reserved: "demote" (via "trash" | "depublish") for an explicit 108 # Reserved: "demote" (via "trash" | "depublish") for an explicit
102 # depublish workflow, if ever built. 109 # depublish workflow, if ever built.
103 # 110 #
diff --git a/app/models/user.rb b/app/models/user.rb
index 5e47ae7d..4d712f6c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -105,6 +105,77 @@ class User < ApplicationRecord
105 def is_admin? 105 def is_admin?
106 !!admin 106 !!admin
107 end 107 end
108
109 # otp_secret present == enrolled. otp_pending_secret holds the secret
110 # between QR display and first-code confirmation. otp_consumed_timestep
111 # makes every accepted code single-use (replay guard within the drift
112 # window).
113
114 def otp_enrolled?
115 otp_secret.present?
116 end
117
118 # Starts (or restarts) enrollment. Returns the provisioning URI the QR
119 # encodes; otp_pending_secret itself doubles as the manual-entry string.
120 def begin_otp_enrollment!
121 update!(:otp_pending_secret => ROTP::Base32.random)
122 pending_otp_provisioning_uri
123 end
124
125 def pending_otp_provisioning_uri
126 return nil if otp_pending_secret.blank?
127 ROTP::TOTP.new(otp_pending_secret, :issuer => OTP_ISSUER)
128 .provisioning_uri(login)
129 end
130
131 # Confirms enrollment with the first generated code. Promotion and
132 # witnessing are one transaction; the consumed timestep is recorded so
133 # the confirmation code cannot be replayed at login.
134 def confirm_otp_enrollment!(code, actor: self)
135 return false if otp_pending_secret.blank?
136 timestep = ROTP::TOTP.new(otp_pending_secret)
137 .verify(code.to_s.strip,
138 :drift_behind => OTP_DRIFT,
139 :drift_ahead => OTP_DRIFT)
140 return false unless timestep
141
142 transaction do
143 update!(:otp_secret => otp_pending_secret,
144 :otp_pending_secret => nil,
145 :otp_consumed_timestep => timestep)
146 NodeAction.record!(:participants => [self], :user => actor,
147 :action => "otp_enroll", :target_login => login)
148 end
149 true
150 end
151
152 # Login-time verification. Each code is accepted at most once.
153 def verify_otp!(code)
154 return false unless otp_enrolled?
155 timestep = ROTP::TOTP.new(otp_secret)
156 .verify(code.to_s.strip,
157 :drift_behind => OTP_DRIFT,
158 :drift_ahead => OTP_DRIFT,
159 :after => otp_consumed_timestep)
160 return false unless timestep
161
162 update!(:otp_consumed_timestep => timestep)
163 true
164 end
165
166 # Self-service disable and administrative reset share one witnessed
167 # teardown; the verb records which of the two it was. The controller
168 # is responsible for the self-service guards (password + current code).
169 def disable_otp!(actor:)
170 verb = (actor == self) ? "otp_disable" : "otp_reset"
171 transaction do
172 update!(:otp_secret => nil, :otp_pending_secret => nil,
173 :otp_consumed_timestep => nil)
174 NodeAction.record!(:participants => [self], :user => actor,
175 :action => verb, :target_login => login)
176 end
177 true
178 end
108 179
109 private 180 private
110 181