package storage import ( "context" "database/sql" "encoding/json" "errors" "fmt" "time" ) var ErrPasswordChanged = errors.New("storage: the password hash changed since it was verified") func (s *Store) ChangePasswordAudited(ctx context.Context, userID, keepSessionID string, oldHash, newHash []byte, audit AppendAuditInput) (Revoked, error) { return s.ChangePasswordAuditedAt(ctx, userID, keepSessionID, oldHash, newHash, audit, s.now()) } func (s *Store) ChangePasswordAuditedAt(ctx context.Context, userID, keepSessionID string, oldHash, newHash []byte, audit AppendAuditInput, now time.Time) (Revoked, error) { const operation = "change password" if err := requireValue("kept session id", keepSessionID); err != nil { return Revoked{}, invalid(operation, err) } if len(oldHash) == 0 || len(newHash) == 0 { return Revoked{}, invalid(operation, fmt.Errorf("password hash is empty")) } fields := map[string]any{} if audit.Detail != "" { if err := json.Unmarshal([]byte(audit.Detail), &fields); err != nil || fields == nil { return Revoked{}, invalid(operation, fmt.Errorf("audit detail is not a JSON object")) } } var revoked Revoked err := s.auditedAt(ctx, operation, audit.Actor, now, func(tx *sql.Tx, at time.Time) error { var owner, username string var dead, disabled int var expires int64 err := tx.QueryRowContext(ctx, ` SELECT f.user_id, f.revoked, f.expires_at, u.username, u.disabled FROM forward_sessions f JOIN users u ON u.id = f.user_id WHERE f.id = ?`, keepSessionID).Scan(&owner, &dead, &expires, &username, &disabled) switch { case errors.Is(err, sql.ErrNoRows): return invalid(operation, fmt.Errorf("kept session does not exist")) case err != nil: return classify(operation, err) case owner != userID: return invalid(operation, fmt.Errorf("kept session does not belong to the user")) case dead != 0 || expires <= timeToInt(at): return invalid(operation, fmt.Errorf("kept session is not live")) case disabled != 0: return invalid(operation, fmt.Errorf("the user is disabled")) } changed, err := execCount(ctx, tx, operation, `UPDATE users SET password_hash = ? WHERE id = ? AND password_hash = ?`, newHash, userID, oldHash) if err != nil { return err } if changed == 0 { return fmt.Errorf("%s: %w", operation, ErrPasswordChanged) } if revoked, err = revokeCredentials(ctx, tx, operation, userID, keepSessionID, true); err != nil { return err } if _, err := endResetLink(ctx, tx, operation, userID); err != nil { return err } fields["username"] = username return writeAudit(ctx, tx, operation, audit.Actor, audit.Action, audit.Subject, at, revoked.into(fields)) }) if err != nil { return Revoked{}, err } return revoked, nil }