1package storage
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "time"
10)
11
12var ErrPasswordChanged = errors.New("storage: the password hash changed since it was verified")
13
14func (s *Store) ChangePasswordAudited(ctx context.Context, userID, keepSessionID string, oldHash, newHash []byte, audit AppendAuditInput) (Revoked, error) {
15 return s.ChangePasswordAuditedAt(ctx, userID, keepSessionID, oldHash, newHash, audit, s.now())
16}
17
18func (s *Store) ChangePasswordAuditedAt(ctx context.Context, userID, keepSessionID string, oldHash, newHash []byte, audit AppendAuditInput, now time.Time) (Revoked, error) {
19 const operation = "change password"
20 if err := requireValue("kept session id", keepSessionID); err != nil {
21 return Revoked{}, invalid(operation, err)
22 }
23 if len(oldHash) == 0 || len(newHash) == 0 {
24 return Revoked{}, invalid(operation, fmt.Errorf("password hash is empty"))
25 }
26 fields := map[string]any{}
27 if audit.Detail != "" {
28 if err := json.Unmarshal([]byte(audit.Detail), &fields); err != nil || fields == nil {
29 return Revoked{}, invalid(operation, fmt.Errorf("audit detail is not a JSON object"))
30 }
31 }
32 var revoked Revoked
33 err := s.auditedAt(ctx, operation, audit.Actor, now, func(tx *sql.Tx, at time.Time) error {
34 var owner, username string
35 var dead, disabled int
36 var expires int64
37 err := tx.QueryRowContext(ctx, `
38SELECT f.user_id, f.revoked, f.expires_at, u.username, u.disabled
39FROM forward_sessions f JOIN users u ON u.id = f.user_id
40WHERE f.id = ?`, keepSessionID).Scan(&owner, &dead, &expires, &username, &disabled)
41 switch {
42 case errors.Is(err, sql.ErrNoRows):
43 return invalid(operation, fmt.Errorf("kept session does not exist"))
44 case err != nil:
45 return classify(operation, err)
46 case owner != userID:
47 return invalid(operation, fmt.Errorf("kept session does not belong to the user"))
48 case dead != 0 || expires <= timeToInt(at):
49 return invalid(operation, fmt.Errorf("kept session is not live"))
50 case disabled != 0:
51 return invalid(operation, fmt.Errorf("the user is disabled"))
52 }
53 changed, err := execCount(ctx, tx, operation, `UPDATE users SET password_hash = ? WHERE id = ? AND password_hash = ?`, newHash, userID, oldHash)
54 if err != nil {
55 return err
56 }
57 if changed == 0 {
58 return fmt.Errorf("%s: %w", operation, ErrPasswordChanged)
59 }
60 if revoked, err = revokeCredentials(ctx, tx, operation, userID, keepSessionID, true); err != nil {
61 return err
62 }
63 if _, err := endResetLink(ctx, tx, operation, userID); err != nil {
64 return err
65 }
66 fields["username"] = username
67 return writeAudit(ctx, tx, operation, audit.Actor, audit.Action, audit.Subject, at, revoked.into(fields))
68 })
69 if err != nil {
70 return Revoked{}, err
71 }
72 return revoked, nil
73}