mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:51:30 +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:
@@ -30,6 +30,22 @@ type AuthHandler struct {
|
||||
perms AuthPermissionService
|
||||
actors AuthActorRoleService
|
||||
checker auth.PermissionChecker
|
||||
// csrfRotator is the optional session-CSRF-rotation hook called
|
||||
// post-role-mutation. Audit 2026-05-10 HIGH-2 closure — when an
|
||||
// actor's role set changes, every active session's CSRF token is
|
||||
// rotated as defense-in-depth against token leak preceding the
|
||||
// privilege change. Nil-safe: when unset (pre-Bundle-2 wiring,
|
||||
// tests that don't care about CSRF), the wires are no-ops.
|
||||
csrfRotator CSRFRotator
|
||||
}
|
||||
|
||||
// CSRFRotator is the projection of *session.Service used by AuthHandler
|
||||
// to rotate CSRF tokens across an actor's active sessions after a role
|
||||
// mutation. RotateCSRFTokenForActor returns the count of rotated rows
|
||||
// and NEVER errors out — rotation is defense-in-depth and must not
|
||||
// block the role mutation that triggered it.
|
||||
type CSRFRotator interface {
|
||||
RotateCSRFTokenForActor(ctx context.Context, actorID, actorType string) int
|
||||
}
|
||||
|
||||
// AuthRoleService is the service-layer dependency the AuthHandler uses
|
||||
@@ -82,6 +98,16 @@ func NewAuthHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// WithCSRFRotator returns a copy of the handler with the CSRF-rotation
|
||||
// hook installed. Audit 2026-05-10 HIGH-2 closure — production wiring
|
||||
// in cmd/server/main.go calls this with the post-Bundle-2
|
||||
// session.Service; pre-Bundle-2 deployments + tests can leave the
|
||||
// rotator nil and the role-mutation handlers simply skip rotation.
|
||||
func (h AuthHandler) WithCSRFRotator(r CSRFRotator) AuthHandler {
|
||||
h.csrfRotator = r
|
||||
return h
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JSON request / response shapes
|
||||
// =============================================================================
|
||||
@@ -410,6 +436,14 @@ func (h AuthHandler) AssignRoleToKey(w http.ResponseWriter, r *http.Request) {
|
||||
writeAuthError(w, err)
|
||||
return
|
||||
}
|
||||
// Audit 2026-05-10 HIGH-2 closure — rotate CSRF across every
|
||||
// active session of the target actor. Non-blocking (per-row
|
||||
// failures are logged inside RotateCSRFTokenForActor but the
|
||||
// return value isn't an error). API-key actors typically have no
|
||||
// sessions (Bearer-only) so this is a no-op for them.
|
||||
if h.csrfRotator != nil {
|
||||
_ = h.csrfRotator.RotateCSRFTokenForActor(r.Context(), keyID, string(domain.ActorTypeAPIKey))
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -426,6 +460,10 @@ func (h AuthHandler) RevokeRoleFromKey(w http.ResponseWriter, r *http.Request) {
|
||||
writeAuthError(w, err)
|
||||
return
|
||||
}
|
||||
// Audit 2026-05-10 HIGH-2 closure — rotate CSRF post-revoke.
|
||||
if h.csrfRotator != nil {
|
||||
_ = h.csrfRotator.RotateCSRFTokenForActor(r.Context(), keyID, string(domain.ActorTypeAPIKey))
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user