Fix SEC-08: make system token issuance atomic

- Add IssueSystemToken() method in internal/db/accounts.go that wraps
  revoke-old, track-new, and upsert-system_tokens in a single SQLite
  transaction
- Update handleTokenIssue in internal/server/server.go to use the new
  atomic method instead of three separate DB calls
- Update IssueServiceToken in internal/grpcserver/tokenservice.go with
  the same fix
- Add TestIssueSystemTokenAtomic test covering first issue and rotation

Security: token issuance now uses a single transaction to prevent
inconsistent state (e.g., old token revoked but new token not tracked)
if a crash occurs between operations. Follows the same pattern as
RenewToken which was already correctly transactional.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 00:43:13 -07:00
parent 586d4e3355
commit 51a5277062
4 changed files with 149 additions and 15 deletions

View File

@@ -72,16 +72,15 @@ func (ts *tokenServiceServer) IssueServiceToken(ctx context.Context, req *mciasv
return nil, status.Error(codes.Internal, "internal error")
}
// Revoke existing system token if any.
// Atomically revoke existing system token (if any), track the new token,
// and update system_tokens — all in a single transaction.
// Security: prevents inconsistent state if a crash occurs mid-operation.
var oldJTI string
existing, err := ts.s.db.GetSystemToken(acct.ID)
if err == nil && existing != nil {
_ = ts.s.db.RevokeToken(existing.JTI, "rotated")
oldJTI = existing.JTI
}
if err := ts.s.db.TrackToken(claims.JTI, acct.ID, claims.IssuedAt, claims.ExpiresAt); err != nil {
return nil, status.Error(codes.Internal, "internal error")
}
if err := ts.s.db.SetSystemToken(acct.ID, claims.JTI, claims.ExpiresAt); err != nil {
if err := ts.s.db.IssueSystemToken(oldJTI, claims.JTI, acct.ID, claims.IssuedAt, claims.ExpiresAt); err != nil {
return nil, status.Error(codes.Internal, "internal error")
}