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