mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 16:48:58 +00:00
fix(auth): wire RevokeAllForActor + RotateCSRFToken to mutation paths
Closes HIGH-1 + HIGH-2 of the 2026-05-10 audit.
HIGH-1: breakglass.Service.SetPassword and RemoveCredential now call
sessions.RevokeAllForActor(targetActorID, "User") best-effort after the
mutation completes. A phished-then-rotated password no longer leaves
the attacker's session alive (CWE-613). Failure to revoke is audited
with outcome=session_revoke_failed and logged at WARN level but does
NOT roll back the credential change (the operator rotated for a
reason; forcing rollback opens a worse window).
- breakglass.SessionMinter interface extended with RevokeAllForActor.
- cmd/server/main.go::breakglassSessionMinterAdapter gains the bridge
to session.Service.RevokeAllForActor.
- stubSessions in service_test.go tracks revokeAllIDs / revokeAllTypes
/ revokeAllErr.
- Three regression tests:
- TestService_SetPassword_RevokesExistingSessions
- TestService_RemoveCredential_RevokesExistingSessions
- TestService_SetPassword_RevokeFailureDoesNotRollback
HIGH-2: New session.Service.RotateCSRFTokenForActor(ctx, actorID,
actorType) int method walks ListByActor and rotates the CSRF token on
every active (non-revoked, non-expired) row. Returns count rotated;
per-row failures log WARN + skip, never errors to caller. New
handler.CSRFRotator interface + AuthHandler.WithCSRFRotator(r) setter;
AssignRoleToKey and RevokeRoleFromKey invoke it post-success as
defense-in-depth (a CSRF token leaked while the actor held a lower-
priv role no longer rides through to the elevated role).
- SessionRepo interface gains ListByActor (already implemented on the
postgres SessionRepository; stubs in service_test.go + bench_test.go
updated to match).
- cmd/server/main.go calls .WithCSRFRotator(sessionService) on the
AuthHandler.
- Two regression tests:
- TestRotateCSRFTokenForActor_RotatesAllActiveRows (asserts revoked /
expired / other-actor rows are skipped)
- TestRotateCSRFTokenForActor_NoSessionsReturnsZero
Verification gate green: gofmt clean, go vet clean, go test -short
-count=1 ./internal/auth/breakglass/ ./internal/auth/session/
./internal/api/handler/ ./internal/api/router/ ./cmd/server/
./internal/domain/auth/ — all pass.
CRIT-1..CRIT-5 + HIGH-1 + HIGH-2 of the 2026-05-10 audit now closed
on this branch. Spec at
cowork/auth-bundles-fixes-2026-05-10/06-high-1-2-revoke-and-rotate.md.
Refs: cowork/auth-bundles-audit-2026-05-10.md HIGH-1 HIGH-2
This commit is contained in:
@@ -72,6 +72,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -173,6 +174,11 @@ var (
|
||||
type SessionRepo interface {
|
||||
Create(ctx context.Context, s *sessiondomain.Session) error
|
||||
Get(ctx context.Context, id string) (*sessiondomain.Session, error)
|
||||
// ListByActor returns every session row for the (actor_id, actor_type)
|
||||
// pair in the tenant. Used by RotateCSRFTokenForActor (Audit
|
||||
// 2026-05-10 HIGH-2). Order is implementation-defined; the caller
|
||||
// filters revoked/expired rows post-fetch.
|
||||
ListByActor(ctx context.Context, actorID, actorType, tenantID string) ([]*sessiondomain.Session, error)
|
||||
UpdateLastSeen(ctx context.Context, id string) error
|
||||
UpdateCSRFTokenHash(ctx context.Context, id, csrfTokenHash string) error
|
||||
Revoke(ctx context.Context, id string) error
|
||||
@@ -553,6 +559,44 @@ func (s *Service) RotateCSRFToken(ctx context.Context, sessionID string) (string
|
||||
return csrfToken, nil
|
||||
}
|
||||
|
||||
// RotateCSRFTokenForActor rotates the CSRF token across every active
|
||||
// (non-revoked) session of the given actor. Returns the count of
|
||||
// successfully rotated rows. Per-row failures are logged + skipped —
|
||||
// the function NEVER returns an error to the caller, because rotation
|
||||
// is defense-in-depth and must not block the role-mutation that
|
||||
// triggered it.
|
||||
//
|
||||
// Audit 2026-05-10 HIGH-2 closure — wires the documented "any actor-
|
||||
// role mutation rotates this actor's CSRF tokens" contract (see
|
||||
// RotateCSRFToken doc block). Pre-fix the rotate primitive existed
|
||||
// but the only call site was Service.Create (login mint).
|
||||
func (s *Service) RotateCSRFTokenForActor(ctx context.Context, actorID, actorType string) int {
|
||||
rows, err := s.sessions.ListByActor(ctx, actorID, actorType, s.tenantID)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "session: list-by-actor for csrf rotate failed",
|
||||
"actor_id", actorID, "actor_type", actorType, "err", err)
|
||||
return 0
|
||||
}
|
||||
rotated := 0
|
||||
now := s.clockNow().UTC()
|
||||
for _, sess := range rows {
|
||||
// Skip revoked / expired rows — they're not consultable anyway.
|
||||
if sess.RevokedAt != nil {
|
||||
continue
|
||||
}
|
||||
if sess.AbsoluteExpiresAt.Before(now) || sess.IdleExpiresAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
if _, rerr := s.RotateCSRFToken(ctx, sess.ID); rerr != nil {
|
||||
slog.WarnContext(ctx, "session: csrf rotate per-row failed",
|
||||
"actor_id", actorID, "session_id", sess.ID, "err", rerr)
|
||||
continue
|
||||
}
|
||||
rotated++
|
||||
}
|
||||
return rotated
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Signing-key lifecycle.
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user