Files
traefikoidc/review_r159_regression_test.go
T
lukaszraczylo 6d96e8869b test: add R-round regression and security suite
- fail-on-old regression tests for every shipped fix (R-rounds)
- fuzz targets (parseJWT, bearer), concurrency-stress, shuffle-flake guards
- coverage-closing contract tests (health-check backend, TLS-posture reuse)
- r154 ID-token fixture built from parts to avoid false-positive secret scan
2026-08-14 16:22:39 +01:00

106 lines
3.8 KiB
Go

package traefikoidc
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
)
// serializingCache simulates a UniversalCache backed by a serializing
// store (e.g. Redis): Set JSON-round-trips the value, so Get returns a
// generic map, never the original concrete type. This is exactly what
// production sees on the distributed path.
type serializingCache struct {
m map[string]interface{}
}
func (c *serializingCache) Set(key string, value any, ttl time.Duration) {
b, _ := json.Marshal(value)
var v interface{}
_ = json.Unmarshal(b, &v)
c.m[key] = v
}
func (c *serializingCache) Get(key string) (any, bool) {
v, ok := c.m[key]
return v, ok
}
func (c *serializingCache) Delete(key string) { delete(c.m, key) }
func (c *serializingCache) SetMaxSize(int) {}
func (c *serializingCache) Size() int { return len(c.m) }
func (c *serializingCache) Clear() { c.m = map[string]interface{}{} }
func (c *serializingCache) Cleanup() {}
func (c *serializingCache) Close() {}
func (c *serializingCache) GetStats() map[string]any { return nil }
// TestR159_RefreshResultDedupAcrossSerializingCache guards the
// lookupCachedRefreshResult cross-backend decode (token_manager.go). The
// refresh-result dedup stored a *TokenResponse but the serializing
// backend returns it as a JSON map, so the *TokenResponse type-assert
// always failed and every replica still posted the refresh token — the
// advertised cross-replica grant coalescing was silently inert. The fix
// normalizes serialized forms back to *TokenResponse.
// Fail-on-old: lookup returns nil/false on a serializing backend.
func TestR159_RefreshResultDedupAcrossSerializingCache(t *testing.T) {
tObj := &TraefikOidc{
refreshResultCache: &serializingCache{m: map[string]interface{}{}},
}
tObj.cacheRefreshResult("session-1", &TokenResponse{
AccessToken: "at-1",
RefreshToken: "rt-rotated",
})
tr, ok := tObj.lookupCachedRefreshResult("session-1")
if !ok || tr == nil {
t.Fatal("refresh result dedup must survive a serializing backend; lookup returned nothing")
}
if tr.AccessToken != "at-1" || tr.RefreshToken != "rt-rotated" {
t.Fatalf("dedup round-trip corrupted the token; got %+v", tr)
}
}
// TestR159_BearerOpaqueTokenIntrospectedWhenRequired guards the bearer
// opaque-token path (bearer_auth.go). The JWT-shape gate
// (strings.Count(token,'.') != 2) ran before the introspection branch,
// so every opaque bearer access token got a 401 even when
// requireTokenIntrospection was enabled for exactly that purpose. The fix
// introspects opaque tokens on the bearer path (mirroring the session
// path's validateOpaqueToken) and forwards with the introspected
// subject.
// Fail-on-old: opaque token rejected (401) despite active introspection.
func TestR159_BearerOpaqueTokenIntrospectedWhenRequired(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"active": true,
"sub": "opaque-user",
"token_type": "Bearer",
})
}))
defer ts.Close()
var nextCalled atomic.Bool
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled.Store(true)
w.WriteHeader(http.StatusOK)
})
oidc := makeBearerOIDC(t, next)
oidc.requireTokenIntrospection = true
oidc.introspectionURL = ts.URL
oidc.httpClient = ts.Client()
oidc.clientSecret = "secret"
req := httptest.NewRequest("GET", "/api/x", nil)
req.Header.Set("Authorization", "Bearer opaque-token-with-no-dots")
rw := httptest.NewRecorder()
oidc.ServeHTTP(rw, req)
if !nextCalled.Load() {
t.Fatalf("opaque token must be introspected and forwarded when requireTokenIntrospection is set; status=%d body=%q", rw.Code, rw.Body.String())
}
}