mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-18 04:47:28 +00:00
Audit review (rounds 1-31): fix confirmed issues, remove dead code, harden tests
Security/correctness fixes: - fail-open token validation preserved with lenient-audience; access token that fails verification and has no ID token to corroborate now fails closed - token_manager blacklist-expiry: fix impossible tautology (x>24h && x<24h) - constant-time OAuth state compare; clear state/nonce/code_verifier on callback token-exchange failure to prevent replay of half-failed flows - StopAllTasks gated on last-instance close - background-task panic recovery; remove task-registry counter leak - circuit breaker half-open: release probe slot; CB + per-session outcome recorded once per refresh operation, not once per waiter - Redis connection pool: handle nil receive when pool closed concurrently - FileStore: atomic temp-write + rename - errors_recovery health-check data race -> atomic Test-isolation / hardening: - reset helpers for Once-pinned global cache manager and transport pool - coverage: utils 70->93% (redirect URL security contract); fuzz targets for bearer parsers; concurrent ServeHTTP race tests for the shared request path Dead-code removal (verified orphaned, build+suite green): internal/providers (30 files, 0 importers), internal/compat, internal/features, input_validation. Verified: govulncheck, go mod tidy, yaegi-validate, cross-compile (darwin+ linux), golangci-lint (new-from-rev clean), 3x full race+shuffle green.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestReviewFailClosed_ExpiredJWTNoIDToken verifies the fail-closed contract:
|
||||
// a JWT access token that fails verification (expired) with no ID token and
|
||||
// no refresh token must NOT authenticate. Previously this path returned
|
||||
// (authenticated=true) — fail-open, trusting an invalid access token.
|
||||
func TestReviewFailClosed_ExpiredJWTNoIDToken(t *testing.T) {
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
// Validly-signed but already-expired access token.
|
||||
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||
"iss": "https://test-issuer.com",
|
||||
"aud": "test-client-id",
|
||||
"exp": float64(time.Now().Add(-1 * time.Hour).Unix()),
|
||||
"iat": float64(time.Now().Add(-2 * time.Hour).Unix()),
|
||||
"sub": "test-user",
|
||||
"jti": "expired-access-jti",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||
session, err := ts.tOidc.sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get session: %v", err)
|
||||
}
|
||||
session.SetAuthenticated(true)
|
||||
session.SetAccessToken(accessToken)
|
||||
// No ID token, no refresh token: nothing left to corroborate the access token.
|
||||
|
||||
rs := (&requestState{}).captureSession(session)
|
||||
authenticated, needsRefresh, expired := ts.tOidc.validateStandardTokensRS(rs)
|
||||
if authenticated {
|
||||
t.Errorf("Expired JWT access token with no ID token must NOT authenticate. Got auth=%v refresh=%v expired=%v",
|
||||
authenticated, needsRefresh, expired)
|
||||
}
|
||||
if !expired {
|
||||
t.Errorf("Expired JWT access token with no ID token should force re-authentication (expired=true). Got expired=%v", expired)
|
||||
}
|
||||
if needsRefresh {
|
||||
t.Errorf("No refresh token present, needsRefresh must be false. Got %v", needsRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReviewFailClosed_ExpiredJWTWithRefreshRequestsRefresh verifies that an
|
||||
// expired JWT access token with no ID token but an available refresh token is
|
||||
// not authenticated but signals a refresh rather than failing open.
|
||||
func TestReviewFailClosed_ExpiredJWTWithRefreshRequestsRefresh(t *testing.T) {
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||
"iss": "https://test-issuer.com",
|
||||
"aud": "test-client-id",
|
||||
"exp": float64(time.Now().Add(-1 * time.Hour).Unix()),
|
||||
"iat": float64(time.Now().Add(-2 * time.Hour).Unix()),
|
||||
"sub": "test-user",
|
||||
"jti": "expired-access-with-refresh-jti",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||
session, err := ts.tOidc.sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get session: %v", err)
|
||||
}
|
||||
session.SetAuthenticated(true)
|
||||
session.SetAccessToken(accessToken)
|
||||
session.SetRefreshToken("refresh-token")
|
||||
|
||||
rs := (&requestState{}).captureSession(session)
|
||||
authenticated, needsRefresh, expired := ts.tOidc.validateStandardTokensRS(rs)
|
||||
if authenticated {
|
||||
t.Errorf("Expired access token must not authenticate even with refresh available. Got auth=%v", authenticated)
|
||||
}
|
||||
if !needsRefresh {
|
||||
t.Errorf("Expired access token with refresh token available should request refresh. Got needsRefresh=%v", needsRefresh)
|
||||
}
|
||||
if expired {
|
||||
t.Errorf("With refresh available, should prefer refresh over forced expiry. Got expired=%v", expired)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReview_ValidJWTAccessTokenNoIDToken_StillAuthenticates guards against
|
||||
// over-correction: a still-valid JWT access token with no ID token must
|
||||
// continue to authenticate (used by access-token-only providers like GitHub).
|
||||
func TestReview_ValidJWTAccessTokenNoIDToken_StillAuthenticates(t *testing.T) {
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||
"iss": "https://test-issuer.com",
|
||||
"aud": "test-client-id",
|
||||
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
|
||||
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
|
||||
"sub": "test-user",
|
||||
"jti": "valid-access-jti",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||
session, err := ts.tOidc.sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get session: %v", err)
|
||||
}
|
||||
session.SetAuthenticated(true)
|
||||
session.SetAccessToken(accessToken)
|
||||
|
||||
rs := (&requestState{}).captureSession(session)
|
||||
authenticated, needsRefresh, expired := ts.tOidc.validateStandardTokensRS(rs)
|
||||
if !authenticated {
|
||||
t.Errorf("Valid JWT access token with no ID token should authenticate. Got auth=%v refresh=%v expired=%v",
|
||||
authenticated, needsRefresh, expired)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user