package storage import ( "context" "database/sql" "encoding/json" "errors" "fmt" "time" ) const ( PendingUserKey = "u" PendingBrowserKey = "b" ) const ( browserLiveRow = `SELECT browser FROM forward_sessions WHERE id = ? AND user_id = ? AND revoked = 0 AND expires_at > ?` browserRowsRevoke = `UPDATE forward_sessions SET revoked = 1 WHERE user_id = ? AND revoked = 0 AND (id = ? OR browser = ?)` browserCodesDelete = `DELETE FROM auth_codes WHERE claims_user_id = ? AND connector_id = ? AND substr(CAST(connector_data AS TEXT), -33) = '.' || ?` 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) = '.' || ?` browserPendingDelete = `DELETE FROM auth_requests WHERE connector_id = ? AND (CASE WHEN json_valid(CAST(connector_data AS TEXT)) THEN json_extract(CAST(connector_data AS TEXT), '$.` + PendingUserKey + `') END) = ? AND (CASE WHEN json_valid(CAST(connector_data AS TEXT)) THEN json_extract(CAST(connector_data AS TEXT), '$.` + PendingBrowserKey + `') END) = ?` ) func (s *Store) RevokeBrowserAudited(ctx context.Context, userID, sessionID string, audit AppendAuditInput) (Revoked, error) { return s.RevokeBrowserAuditedAt(ctx, userID, sessionID, audit, s.now()) } func (s *Store) RevokeBrowserAuditedAt(ctx context.Context, userID, sessionID string, audit AppendAuditInput, now time.Time) (Revoked, error) { const operation = "revoke browser sessions" 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 browser sql.NullString err := tx.QueryRowContext(ctx, browserLiveRow, sessionID, userID, timeToInt(at)).Scan(&browser) switch { case errors.Is(err, sql.ErrNoRows): return invalid(operation, fmt.Errorf("the session is not a live session of the user")) case err != nil: return classify(operation, err) } if revoked.Sessions, err = execCount(ctx, tx, operation, browserRowsRevoke, userID, sessionID, browser); err != nil { return err } if browser.Valid { if revoked.Codes, err = execCount(ctx, tx, operation+" dex auth codes", browserCodesDelete, userID, DexConnectorID, browser.String); err != nil { return err } granted, err := execCount(ctx, tx, operation+" dex spent and logged-in auth requests", browserGrantDelete, DexConnectorID, userID, userID, browser.String) if err != nil { return err } pending, err := execCount(ctx, tx, operation+" dex pending auth requests", browserPendingDelete, DexConnectorID, userID, browser.String) if err != nil { return err } revoked.PendingSignIns = granted + pending } return writeAudit(ctx, tx, operation, audit.Actor, audit.Action, audit.Subject, at, revoked.into(fields)) }) if err != nil { return Revoked{}, err } return revoked, nil }