+66
-28
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"oci-portal/internal/model"
|
||||
)
|
||||
@@ -79,52 +80,89 @@ func (s *AuthService) SetupTotp(ctx context.Context, username string) (secret, u
|
||||
return key.Secret(), key.URL(), nil
|
||||
}
|
||||
|
||||
// ActivateTotp 校验暂存密钥的验证码,通过后加密落库启用。
|
||||
func (s *AuthService) ActivateTotp(ctx context.Context, username, code string) error {
|
||||
// ActivateTotp 校验暂存密钥并在同一事务内启用、递增版本、接续当前会话。
|
||||
func (s *AuthService) ActivateTotp(
|
||||
ctx context.Context, username, code, oldToken string, meta SessionMeta, proof TokenProof,
|
||||
) (string, time.Time, error) {
|
||||
s.totpMu.Lock()
|
||||
pending, ok := s.totpPending[username]
|
||||
s.totpMu.Unlock()
|
||||
if !ok || time.Now().After(pending.expires) {
|
||||
return ErrTotpNotSetup
|
||||
return "", time.Time{}, ErrTotpNotSetup
|
||||
}
|
||||
if !totp.Validate(code, pending.secret) {
|
||||
return ErrTotpInvalid
|
||||
return "", time.Time{}, ErrTotpInvalid
|
||||
}
|
||||
enc, err := s.cipher.EncryptString(pending.secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt totp secret: %w", err)
|
||||
return "", time.Time{}, fmt.Errorf("encrypt totp secret: %w", err)
|
||||
}
|
||||
err = s.db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("username = ?", username).Update("totp_secret_enc", enc).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("save totp secret: %w", err)
|
||||
token, expires, err := s.changeTotp(ctx, username, enc, oldToken, meta, proof, nil)
|
||||
if err == nil {
|
||||
s.deleteTotpPending(username, pending.secret)
|
||||
}
|
||||
s.totpMu.Lock()
|
||||
delete(s.totpPending, username)
|
||||
s.totpMu.Unlock()
|
||||
// 两步验证形态变更:旧令牌全部失效
|
||||
return s.bumpTokenVersion(ctx, username)
|
||||
return token, expires, err
|
||||
}
|
||||
|
||||
// DisableTotp 停用两步验证;需当前验证码或登录密码任一确认。
|
||||
func (s *AuthService) DisableTotp(ctx context.Context, username, password, code string) error {
|
||||
user, err := s.findUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.TotpSecretEnc == "" {
|
||||
func (s *AuthService) DisableTotp(
|
||||
ctx context.Context, username, password, code, oldToken string, meta SessionMeta, proof TokenProof,
|
||||
) (string, time.Time, error) {
|
||||
confirm := func(user *model.User) error {
|
||||
if user.TotpSecretEnc != "" && !s.confirmDisable(user, password, code) {
|
||||
return ErrTotpConfirm
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !s.confirmDisable(user, password, code) {
|
||||
return ErrTotpConfirm
|
||||
return s.changeTotp(ctx, username, "", oldToken, meta, proof, confirm)
|
||||
}
|
||||
|
||||
func (s *AuthService) changeTotp(
|
||||
ctx context.Context, username, secret, oldToken string, meta SessionMeta,
|
||||
proof TokenProof, confirm func(*model.User) error,
|
||||
) (string, time.Time, error) {
|
||||
var token string
|
||||
var expires time.Time
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
user, err := lockUserForAuthChange(tx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.ensureTokenCurrentTx(tx, user, proof); err != nil {
|
||||
return err
|
||||
}
|
||||
if confirm != nil {
|
||||
if err := confirm(user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.persistTotpTx(tx, user, secret, oldToken, meta, &token, &expires)
|
||||
})
|
||||
return token, expires, err
|
||||
}
|
||||
|
||||
func (s *AuthService) persistTotpTx(
|
||||
tx *gorm.DB, user *model.User, secret, oldToken string, meta SessionMeta,
|
||||
token *string, expires *time.Time,
|
||||
) error {
|
||||
if err := tx.Model(user).Update("totp_secret_enc", secret).Error; err != nil {
|
||||
return fmt.Errorf("save totp secret: %w", err)
|
||||
}
|
||||
err = s.db.WithContext(ctx).Model(&model.User{}).
|
||||
Where("username = ?", username).Update("totp_secret_enc", "").Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("clear totp secret: %w", err)
|
||||
if err := bumpTokenVersionTx(tx, user.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
user.TokenVersion++
|
||||
var err error
|
||||
*token, *expires, err = s.renewSessionTx(tx, user, oldToken, meta)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *AuthService) deleteTotpPending(username, secret string) {
|
||||
s.totpMu.Lock()
|
||||
defer s.totpMu.Unlock()
|
||||
if p, ok := s.totpPending[username]; ok && p.secret == secret {
|
||||
delete(s.totpPending, username)
|
||||
}
|
||||
// 两步验证形态变更:旧令牌全部失效
|
||||
return s.bumpTokenVersion(ctx, username)
|
||||
}
|
||||
|
||||
// confirmDisable 校验停用凭证:验证码或密码任一通过即可。
|
||||
|
||||
Reference in New Issue
Block a user