1package storage
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "time"
10)
11
12const (
13 PendingUserKey = "u"
14 PendingBrowserKey = "b"
15)
16
17const (
18 browserLiveRow = `SELECT browser FROM forward_sessions WHERE id = ? AND user_id = ? AND revoked = 0 AND expires_at > ?`
19 browserRowsRevoke = `UPDATE forward_sessions SET revoked = 1 WHERE user_id = ? AND revoked = 0 AND (id = ? OR browser = ?)`
20 browserCodesDelete = `DELETE FROM auth_codes WHERE claims_user_id = ? AND connector_id = ? AND substr(CAST(connector_data AS TEXT), -33) = '.' || ?`
21 browserGrantDelete = `DELETE FROM auth_requests WHERE connector_id = ? AND substr(CAST(connector_data AS TEXT), 1, length(?) + 1) = ? || '.' AND substr(CAST(connector_data AS TEXT), -33) = '.' || ?`
22 browserPendingDelete = `DELETE FROM auth_requests WHERE connector_id = ?
23 AND (CASE WHEN json_valid(CAST(connector_data AS TEXT)) THEN json_extract(CAST(connector_data AS TEXT), '$.` + PendingUserKey + `') END) = ?
24 AND (CASE WHEN json_valid(CAST(connector_data AS TEXT)) THEN json_extract(CAST(connector_data AS TEXT), '$.` + PendingBrowserKey + `') END) = ?`
25)
26
27func (s *Store) RevokeBrowserAudited(ctx context.Context, userID, sessionID string, audit AppendAuditInput) (Revoked, error) {
28 return s.RevokeBrowserAuditedAt(ctx, userID, sessionID, audit, s.now())
29}
30
31func (s *Store) RevokeBrowserAuditedAt(ctx context.Context, userID, sessionID string, audit AppendAuditInput, now time.Time) (Revoked, error) {
32 const operation = "revoke browser sessions"
33 fields := map[string]any{}
34 if audit.Detail != "" {
35 if err := json.Unmarshal([]byte(audit.Detail), &fields); err != nil || fields == nil {
36 return Revoked{}, invalid(operation, fmt.Errorf("audit detail is not a JSON object"))
37 }
38 }
39 var revoked Revoked
40 err := s.auditedAt(ctx, operation, audit.Actor, now, func(tx *sql.Tx, at time.Time) error {
41 var browser sql.NullString
42 err := tx.QueryRowContext(ctx, browserLiveRow, sessionID, userID, timeToInt(at)).Scan(&browser)
43 switch {
44 case errors.Is(err, sql.ErrNoRows):
45 return invalid(operation, fmt.Errorf("the session is not a live session of the user"))
46 case err != nil:
47 return classify(operation, err)
48 }
49 if revoked.Sessions, err = execCount(ctx, tx, operation, browserRowsRevoke, userID, sessionID, browser); err != nil {
50 return err
51 }
52 if browser.Valid {
53 if revoked.Codes, err = execCount(ctx, tx, operation+" dex auth codes", browserCodesDelete, userID, DexConnectorID, browser.String); err != nil {
54 return err
55 }
56 granted, err := execCount(ctx, tx, operation+" dex spent and logged-in auth requests", browserGrantDelete, DexConnectorID, userID, userID, browser.String)
57 if err != nil {
58 return err
59 }
60 pending, err := execCount(ctx, tx, operation+" dex pending auth requests", browserPendingDelete, DexConnectorID, userID, browser.String)
61 if err != nil {
62 return err
63 }
64 revoked.PendingSignIns = granted + pending
65 }
66 return writeAudit(ctx, tx, operation, audit.Actor, audit.Action, audit.Subject, at, revoked.into(fields))
67 })
68 if err != nil {
69 return Revoked{}, err
70 }
71 return revoked, nil
72}