diff options
Diffstat (limited to 'app/models/user.rb')
| -rw-r--r-- | app/models/user.rb | 71 |
1 files changed, 71 insertions, 0 deletions
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 | ||
