@@ -115,7 +115,8 @@ func PostLogin(c *gin.Context) {
115115 return
116116 }
117117
118- usedStep , valid := verifyPasscode (user .TFASecret , params .Passcode , user .TFALastUsedStep , time .Now ())
118+ now := time .Now ()
119+ usedStep , valid := verifyPasscode (user .TFASecret , params .Passcode , user .TFALastUsedStep , now )
119120 if ! valid {
120121 loginThrottle .fail (keys )
121122 logger .Warnf ("Failed login attempt from %s" , c .ClientIP ())
@@ -137,17 +138,25 @@ func PostLogin(c *gin.Context) {
137138 return
138139 }
139140
140- loginThrottle .reset (keys )
141+ // The passcode is spent here, in one conditional update, and the session
142+ // below is only created when this request is the one that spent it. A plain
143+ // Save after the check would leave a window in which two requests that
144+ // carry the same passcode both read the same stored time step and both
145+ // accept it.
146+ consumed , err := consumePasscode (db , & user , usedStep , now .Unix ()/ int64 (OtpOpts .Period ), now )
147+ if err != nil {
148+ logger .Error (err )
149+ response .Error (c , response.JSONBody {Err : response .ErrInternalError , Msg : "Error occurred when creating sessions" }, http .StatusInternalServerError )
150+ return
151+ }
152+ if ! consumed {
153+ loginThrottle .fail (keys )
154+ logger .Warnf ("Refused a login from %s: the passcode of time step %d was already used" , c .ClientIP (), usedStep )
155+ response .Error (c , response.JSONBody {Err : response .ErrWrongPasscode , Msg : "Failed to verify your passcode" }, http .StatusUnauthorized )
156+ return
157+ }
141158
142- user .LastLoginTime = time .Now ().Unix ()
143- user .IsEnabled = true
144- // The bootstrap is over: the secret is bound, so it is no longer pending
145- // and no longer tied to the address that fetched it.
146- user .TFABootstrapIP = ""
147- // Remember the time step of the accepted passcode so that the same one
148- // cannot be used a second time.
149- user .TFALastUsedStep = usedStep
150- db .Save (& user )
159+ loginThrottle .reset (keys )
151160
152161 session := sessions .Default (c )
153162 session .Options (sessionopts .Options (c ))
@@ -214,11 +223,18 @@ func GetOTPUrl(c *gin.Context) {
214223 // An installation that already carries a pending secret from a
215224 // release that did not record the address keeps working: the first
216225 // caller adopts it, exactly as it did before.
217- user .TFABootstrapIP = clientIP
218- db .Save (& user )
226+ adopted , err := adoptBootstrapSecret (db , user .TFASecret , clientIP )
227+ if err != nil {
228+ logger .Error (err )
229+ response .Error (c , response.JSONBody {Err : response .ErrInternalError , Msg : "Error occurred when generating otp qrcode" }, http .StatusInternalServerError )
230+ return
231+ }
232+ if ! adopted {
233+ refusePendingSecret (c , clientIP )
234+ return
235+ }
219236 } else if user .TFABootstrapIP != clientIP {
220- logger .Warnf ("Refused to hand out the pending TFA secret to %s: it was issued to %s" , clientIP , user .TFABootstrapIP )
221- response .Error (c , response.JSONBody {Err : response .ErrLoginRequired , Msg : "The pending TFA secret belongs to another address, run 'mgt -reset_user <user>' to bind a new one" }, http .StatusForbidden )
237+ refusePendingSecret (c , clientIP )
222238 return
223239 }
224240
@@ -234,13 +250,109 @@ func GetOTPUrl(c *gin.Context) {
234250 return
235251 }
236252
237- user .TFASecret = otpKey .Secret ()
238- user .TFABootstrapIP = clientIP
239- db .Save (& user )
253+ // The secret is installed by an update that only matches while the row
254+ // still has none, so two calls that arrive together cannot both be told
255+ // that the secret they generated is the one the account carries: the
256+ // caller that loses reads the stored secret below and hands that one out
257+ // when it belongs to the same address, which is what a console that
258+ // retries produces, and is refused otherwise.
259+ issued , err := claimBootstrapSecret (db , clientIP , otpKey .Secret ())
260+ if err != nil {
261+ logger .Error (err )
262+ response .Error (c , response.JSONBody {Err : response .ErrInternalError , Msg : "Error occurred when generating otp qrcode" }, http .StatusInternalServerError )
263+ return
264+ }
265+ if ! issued {
266+ logger .Warnf ("Lost the TFA bootstrap race to another caller, %s is served the stored secret if it is its own" , clientIP )
267+ var stored model.User
268+ db .Where (& model.User {Username : constants .SuperUser }).First (& stored )
269+ if stored .TFASecret == "" || stored .TFABootstrapIP != clientIP {
270+ refusePendingSecret (c , clientIP )
271+ return
272+ }
273+
274+ response .Success (c , gin.H {"url" : otpURL (stored .TFASecret )})
275+ return
276+ }
277+
240278 logger .Warnf ("Issued a new TFA secret to %s; it can only be bound and used from that address until the first login" , clientIP )
241279 response .Success (c , gin.H {"url" : otpKey .URL ()})
242280}
243281
282+ // refusePendingSecret answers a caller that may not be handed the pending TFA
283+ // secret, because it was issued to another address.
284+ func refusePendingSecret (c * gin.Context , clientIP string ) {
285+ logger .Warnf ("Refused to hand out the pending TFA secret to %s: it belongs to another address" , clientIP )
286+ response .Error (c , response.JSONBody {Err : response .ErrLoginRequired , Msg : "The pending TFA secret belongs to another address, run 'mgt -reset_user <user>' to bind a new one" }, http .StatusForbidden )
287+ }
288+
289+ // claimBootstrapSecret installs secret as the pending TFA secret of the
290+ // account and reports whether this call is the one that installed it.
291+ //
292+ // The update matches only while the row carries no secret yet, which is what
293+ // makes the claim exclusive. Reading the row and writing it back would let two
294+ // callers that arrive while the account has no secret both store the one they
295+ // generated, so both would be told the account is bound to the secret they
296+ // hold, while the database keeps only the last write.
297+ func claimBootstrapSecret (db * database.PostgresDB , clientIP , secret string ) (bool , error ) {
298+ result := db .Model (& model.User {}).
299+ Where ("username = ? AND (tfa_secret = '' OR tfa_secret IS NULL)" , constants .SuperUser ).
300+ Updates (map [string ]interface {}{
301+ "tfa_secret" : secret ,
302+ "tfa_bootstrap_ip" : clientIP ,
303+ })
304+ if result .Error != nil {
305+ return false , result .Error
306+ }
307+
308+ return result .RowsAffected == 1 , nil
309+ }
310+
311+ // adoptBootstrapSecret records clientIP as the address of a pending secret
312+ // that was stored before the address was recorded.
313+ //
314+ // The update is restricted to the secret that was read, so a caller that lost a
315+ // race against the installation of a new secret adopts nothing.
316+ func adoptBootstrapSecret (db * database.PostgresDB , pending , clientIP string ) (bool , error ) {
317+ result := db .Model (& model.User {}).
318+ Where ("username = ? AND tfa_secret = ? AND (tfa_bootstrap_ip = '' OR tfa_bootstrap_ip IS NULL)" , constants .SuperUser , pending ).
319+ Updates (map [string ]interface {}{
320+ "tfa_bootstrap_ip" : clientIP ,
321+ })
322+ if result .Error != nil {
323+ return false , result .Error
324+ }
325+
326+ return result .RowsAffected == 1 , nil
327+ }
328+
329+ // consumePasscode records an accepted passcode together with the login it
330+ // belongs to, as a single conditional update.
331+ //
332+ // The update is what decides whether the time step of the passcode was already
333+ // spent: it only matches while the stored step is below the accepted one, so
334+ // the second of two concurrent requests that carry the same passcode affects no
335+ // row and is treated as a replay. currentStep is the step the passcode was
336+ // verified against; a stored step ahead of it means the clock moved backwards,
337+ // which this process treats as stale (see verifyPasscode) and repairs here.
338+ func consumePasscode (db * database.PostgresDB , user * model.User , usedStep , currentStep int64 , now time.Time ) (bool , error ) {
339+ result := db .Model (& model.User {}).
340+ Where ("id = ? AND (tfa_last_used_step < ? OR tfa_last_used_step > ?)" , user .ID , usedStep , currentStep + 1 ).
341+ Updates (map [string ]interface {}{
342+ "tfa_last_used_step" : usedStep ,
343+ // The bootstrap is over: the secret is bound, so it is no longer
344+ // pending and no longer tied to the address that fetched it.
345+ "tfa_bootstrap_ip" : "" ,
346+ "last_login_time" : now .Unix (),
347+ "is_enabled" : true ,
348+ })
349+ if result .Error != nil {
350+ return false , result .Error
351+ }
352+
353+ return result .RowsAffected == 1 , nil
354+ }
355+
244356// verifyPasscode checks a TOTP passcode and returns the time step it belongs to.
245357//
246358// lastUsedStep is the step of the passcode that was accepted last; every step
0 commit comments