mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
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.
128 lines
4.6 KiB
Go
128 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|