feat(mfa): second-factor sign-in with TOTP and recovery codes
This commit is contained in:
@@ -290,6 +290,51 @@ func ClearMFA(instanceID, userID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UseRecoveryCode consumes one unused code, marking it used by index so a
|
||||
// concurrent second attempt with the same code finds it spent.
|
||||
func UseRecoveryCode(instanceID, userID, input string) error {
|
||||
m, err := GetUserMFA(instanceID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m == nil || len(m.RecoveryCodes) == 0 {
|
||||
return ErrNoMFA
|
||||
}
|
||||
idx, ok := ConsumeRecoveryCode(m.RecoveryCodes, input, time.Now())
|
||||
if !ok {
|
||||
return ErrBadCode
|
||||
}
|
||||
ctx, cancel := mfaCtx()
|
||||
defer cancel()
|
||||
now := time.Now()
|
||||
res, err := db.Col("user_mfa").UpdateOne(ctx,
|
||||
bson.M{
|
||||
"instance_id": instanceID, "user_id": userID,
|
||||
"recovery_codes." + strconv.Itoa(idx) + ".used_at": bson.M{"$exists": false},
|
||||
},
|
||||
bson.M{"$set": bson.M{
|
||||
"recovery_codes." + strconv.Itoa(idx) + ".used_at": now,
|
||||
"updated_at": now,
|
||||
}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return ErrBadCode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequireMFAForInstance reads the policy, defaulting to off on any error: a
|
||||
// database blip must not lock an entire instance out of its own control plane.
|
||||
func RequireMFAForInstance(instanceID string) bool {
|
||||
s, err := GetSettings(instanceID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return models.RequireMFA(s)
|
||||
}
|
||||
|
||||
// burnTOTPCode makes a step single-use for 90 seconds - longer than the +-1
|
||||
// step window it could still validate in. Keyed on the time step the code
|
||||
// was accepted against, never on the code itself: a raw code sitting in a
|
||||
|
||||
Reference in New Issue
Block a user