Fix SEC-03: require token proximity for renewal

- Add 50% lifetime elapsed check to REST handleRenew and gRPC RenewToken
- Reject renewal attempts before 50% of token lifetime has elapsed
- Update existing renewal tests to use short-lived tokens with sleep
- Add TestRenewTokenTooEarly tests for both REST and gRPC

Security: Tokens can only be renewed after 50% of their lifetime has
elapsed, preventing indefinite renewal of stolen tokens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 00:45:35 -07:00
parent 586d4e3355
commit eef7d1bc1a
5 changed files with 99 additions and 15 deletions

View File

@@ -18,6 +18,7 @@ import (
"log/slog"
"net"
"net/http"
"time"
"git.wntrmute.dev/kyle/mcias/internal/auth"
"git.wntrmute.dev/kyle/mcias/internal/config"
@@ -337,6 +338,15 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleRenew(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context())
// Security: only allow renewal when the token has consumed at least 50% of
// its lifetime. This prevents indefinite renewal of stolen tokens (SEC-03).
totalLifetime := claims.ExpiresAt.Sub(claims.IssuedAt)
elapsed := time.Since(claims.IssuedAt)
if elapsed < totalLifetime/2 {
middleware.WriteError(w, http.StatusBadRequest, "token is not yet eligible for renewal", "renewal_too_early")
return
}
// Load account to get current roles (they may have changed since token issuance).
acct, err := s.db.GetAccountByUUID(claims.Subject)
if err != nil {