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:
@@ -563,7 +563,8 @@ func TestRenewToken(t *testing.T) {
|
||||
acct := createTestHumanAccount(t, srv, "renew-user")
|
||||
handler := srv.Handler()
|
||||
|
||||
oldTokenStr, claims, err := token.IssueToken(priv, testIssuer, acct.UUID, nil, time.Hour)
|
||||
// Issue a short-lived token (2s) so we can wait past the 50% threshold.
|
||||
oldTokenStr, claims, err := token.IssueToken(priv, testIssuer, acct.UUID, nil, 2*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("IssueToken: %v", err)
|
||||
}
|
||||
@@ -572,6 +573,9 @@ func TestRenewToken(t *testing.T) {
|
||||
t.Fatalf("TrackToken: %v", err)
|
||||
}
|
||||
|
||||
// Wait for >50% of the 2s lifetime to elapse.
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
|
||||
rr := doRequest(t, handler, "POST", "/v1/auth/renew", nil, oldTokenStr)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("renew status = %d, want 200; body: %s", rr.Code, rr.Body.String())
|
||||
@@ -594,3 +598,29 @@ func TestRenewToken(t *testing.T) {
|
||||
t.Error("old token should be revoked after renewal")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenewTokenTooEarly verifies that a token cannot be renewed before 50%
|
||||
// of its lifetime has elapsed (SEC-03).
|
||||
func TestRenewTokenTooEarly(t *testing.T) {
|
||||
srv, _, priv, _ := newTestServer(t)
|
||||
acct := createTestHumanAccount(t, srv, "renew-early-user")
|
||||
handler := srv.Handler()
|
||||
|
||||
// Issue a long-lived token so 50% is far in the future.
|
||||
tokStr, claims, err := token.IssueToken(priv, testIssuer, acct.UUID, nil, time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("IssueToken: %v", err)
|
||||
}
|
||||
if err := srv.db.TrackToken(claims.JTI, acct.ID, claims.IssuedAt, claims.ExpiresAt); err != nil {
|
||||
t.Fatalf("TrackToken: %v", err)
|
||||
}
|
||||
|
||||
// Immediately try to renew — should be rejected.
|
||||
rr := doRequest(t, handler, "POST", "/v1/auth/renew", nil, tokStr)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("renew status = %d, want 400; body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "not yet eligible for renewal") {
|
||||
t.Errorf("expected eligibility message, got: %s", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user