mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
feat(middleware): per-request context object (requestState)
Adds requeststate.go and threads a *requestState through the ServeHTTP -> processAuthorizedRequestRS -> forwardAuthorized path. rs is allocated once at the top of ServeHTTP, populates SessionData field snapshots under a SINGLE sd.sessionMutex.RLock, and caches the MetadataSnapshot. Downstream handlers read the cached fields instead of calling session.GetX() / t.metadataSnap() repeatedly. Why --- Under Yaegi each method dispatch (including RWMutex.RLock) costs ~1-5ms of interpreter overhead. SessionData getters each take an RLock on sd.sessionMutex; the previous hot path called 5-7 of them per request (GetAuthenticated, GetAccessToken, GetIDToken, GetRefreshToken, GetUserIdentifier, plus the same set again inside processAuthorizedRequest). With one batched RLock + cached fields, that drops to a single RLock for the whole handler chain. This is scoped — not a wholesale architectural refactor: * requestState is per-request (alloc at ServeHTTP entry, dropped on return). It is NOT a shared cache and never escapes the request. * The original processAuthorizedRequest is kept unchanged for any callers we don't migrate this round (bearer path, callback handlers, expired-token handlers). New code path is the RS-aware processAuthorizedRequestRS, which middleware.ServeHTTP now uses for the happy authenticated-and-not-needing-refresh case. * Cross-request caches (tokenCache, JWKCache, sessionEntries, sessionInvalidationCache) are unchanged. rs is additive, not a replacement. What this does NOT change ------------------------- * The refresh path still calls session.GetX() in middleware.go (handleExpiredToken, refreshToken, defaultInitiateAuthentication) because those flows can mutate session state and a stale rs would be wrong. * validateStandardTokens still has its own session.GetX() calls. Deep plumbing into the token-verification path is a follow-up. * No semantic changes to authentication, refresh, or session lifecycle — only the read path is optimised. All tests pass with -race; golangci-lint clean.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// Package traefikoidc provides OIDC authentication middleware for Traefik.
|
||||
// requestState bundles read-mostly fields for a single ServeHTTP call.
|
||||
package traefikoidc
|
||||
|
||||
import "net/http"
|
||||
|
||||
// requestState is a per-request context object allocated at the top of
|
||||
// ServeHTTP and threaded through to downstream handlers. It caches values
|
||||
// that would otherwise require a Yaegi-dispatched lock acquisition each time
|
||||
// they're read:
|
||||
//
|
||||
// - The metadata snapshot (atomic.Value.Load once, not per-handler).
|
||||
// - SessionData getter results (one RLock on sd.sessionMutex covers all
|
||||
// fields, instead of 5-7 separate RLock/RUnlock pairs scattered through
|
||||
// the handler chain).
|
||||
//
|
||||
// The struct is alloc'd at request entry, populated under at most one RLock
|
||||
// of sd.sessionMutex, and discarded at request exit. It is NOT shared across
|
||||
// requests and never written from another goroutine, so no synchronization
|
||||
// on its fields is required.
|
||||
//
|
||||
// Cross-request global caches (tokenCache, JWKCache, sessionEntries,
|
||||
// sessionInvalidationCache) remain — they're orthogonal. requestState's job
|
||||
// is to eliminate redundant per-handler reads of values that don't change
|
||||
// within a single request.
|
||||
type requestState struct {
|
||||
// Globals snapshotted once.
|
||||
metadata *MetadataSnapshot
|
||||
|
||||
// SessionData fields snapshotted under one RLock. The pointer to the
|
||||
// SessionData is retained so handlers that genuinely need to mutate
|
||||
// (Save, Clear, etc.) still have access.
|
||||
session *SessionData
|
||||
|
||||
authenticated bool
|
||||
accessToken string
|
||||
idToken string
|
||||
refreshToken string
|
||||
userIdentifier string
|
||||
createdAtUnixSec int64
|
||||
|
||||
// Output: scheme/host/redirect path determined at top of ServeHTTP.
|
||||
scheme string
|
||||
host string
|
||||
redirectURL string
|
||||
|
||||
// Carry the next handler so forwardAuthorized doesn't need to close over t.
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
// captureSession populates requestState's SessionData-derived fields under a
|
||||
// single RLock of sd.sessionMutex. Returns the populated rs for chaining.
|
||||
//
|
||||
// Replaces a sequence of SessionData.GetX() calls each of which acquires
|
||||
// sd.sessionMutex.RLock(). Under Yaegi each RLock costs ~1-5ms of
|
||||
// interpreter dispatch; batching saves the rest.
|
||||
func (rs *requestState) captureSession(sd *SessionData) *requestState {
|
||||
if sd == nil {
|
||||
return rs
|
||||
}
|
||||
rs.session = sd
|
||||
sd.sessionMutex.RLock()
|
||||
rs.authenticated = sd.getAuthenticatedUnsafe()
|
||||
rs.accessToken = sd.getAccessTokenUnsafe()
|
||||
rs.idToken = sd.getIDTokenUnsafe()
|
||||
rs.refreshToken = sd.getRefreshTokenUnsafe()
|
||||
rs.userIdentifier = sd.getUserIdentifierUnsafe()
|
||||
rs.createdAtUnixSec = sd.getCreatedAtUnsafe()
|
||||
sd.sessionMutex.RUnlock()
|
||||
return rs
|
||||
}
|
||||
Reference in New Issue
Block a user