mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
fix: reduce yaegi CPU footprint + require auth on SSE/WebSocket bypass
minor-release Behaviour changes (potentially breaking for operators relying on the prior unauthenticated SSE bypass): * SSE (`Accept: text/event-stream`) and WebSocket upgrade requests now return 401 when no authenticated session is present. Previously the bypass forwarded unconditionally, which let any caller reach the backend by setting the right header. Excluded URLs are unchanged. Operators relying on unauthenticated SSE/WS access must move the path into ExcludedURLs. Performance fixes (target: long-running dashboards like Grafana / ArgoCD where many panels poll concurrently while the page stays open): * Stop honouring isTestMode() for the singleton-token-cleanup interval under yaegi (the Traefik plugin runtime). In production the plugin was running a 20 Hz no-op cleanup ticker because runtime.Compiler == "yaegi" tripped the test-mode branch. * processAuthorizedRequest now resolves ID-token claims at most once per request via SessionData.GetIDTokenClaims (already cached on the session) and reuses them for both groups/roles extraction and header-template rendering. Previously every authenticated request parsed the JWT twice. * Added extractGroupsAndRolesFromClaims to drive groups/roles off pre-parsed claims; extractGroupsAndRoles still works for tests. * Removed the unconditional session.MarkDirty() in the header-templates branch. Templates only mutate request headers, not session state, so the prior MarkDirty was re-encrypting and rewriting all session cookies on every authenticated request that used header templates. Other: * Added isWebSocketUpgrade (RFC 6455 handshake detection — Connection: Upgrade + Upgrade: websocket, tolerant of multi-token Connection headers and case). * Renamed applySSEUserHeaders -> applyBypassUserHeaders; it now returns bool so the dispatcher can reject unauthenticated SSE/WS with 401. * Added tests for SSE and WS bypass covering both the auth-rejection path and the authenticated forward path.
This commit is contained in:
+19
-12
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -1193,9 +1194,14 @@ func (t *TraefikOidc) startTokenCleanup() {
|
||||
sessionManager := t.sessionManager
|
||||
logger := t.logger
|
||||
|
||||
// Only use the fast cleanup interval when actually running under `go test`.
|
||||
// runtime.Compiler == "yaegi" makes isTestMode() return true in production
|
||||
// (Traefik interprets the plugin via yaegi), which would otherwise pin this
|
||||
// ticker to 20 Hz on a real cluster despite tokenCache.Cleanup and
|
||||
// jwkCache.Cleanup both being no-ops there.
|
||||
cleanupInterval := 1 * time.Minute
|
||||
if isTestMode() {
|
||||
cleanupInterval = 50 * time.Millisecond // Fast interval for tests
|
||||
if isTestMode() && runtime.Compiler != "yaegi" {
|
||||
cleanupInterval = 50 * time.Millisecond
|
||||
}
|
||||
|
||||
// Create cleanup function
|
||||
@@ -1237,25 +1243,27 @@ func (t *TraefikOidc) startTokenCleanup() {
|
||||
}
|
||||
|
||||
// extractGroupsAndRoles extracts group and role information from token claims.
|
||||
// It parses the 'groups' and 'roles' claims from the ID token and validates their format.
|
||||
// Parameters:
|
||||
// - idToken: The ID token containing claims to extract.
|
||||
// It parses the configured group/role claims from the supplied ID token.
|
||||
//
|
||||
// Returns:
|
||||
// - groups: Array of group names from the 'groups' claim.
|
||||
// - roles: Array of role names from the 'roles' claim.
|
||||
// - An error if claim extraction fails or if the 'groups' or 'roles' claims are present
|
||||
// but not arrays of strings.
|
||||
// Most callers should prefer extractGroupsAndRolesFromClaims when claims have
|
||||
// already been parsed for the request (e.g. via SessionData.GetIDTokenClaims),
|
||||
// to avoid re-parsing the JWT.
|
||||
func (t *TraefikOidc) extractGroupsAndRoles(idToken string) ([]string, []string, error) {
|
||||
claims, err := t.extractClaimsFunc(idToken)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to extract claims: %w", err)
|
||||
}
|
||||
return t.extractGroupsAndRolesFromClaims(claims)
|
||||
}
|
||||
|
||||
// extractGroupsAndRolesFromClaims extracts group and role information from
|
||||
// already-parsed claims. Hot path: callers that have a cached claims map (such
|
||||
// as SessionData.GetIDTokenClaims) should use this to skip a redundant
|
||||
// base64+JSON decode of the JWT on every authenticated request.
|
||||
func (t *TraefikOidc) extractGroupsAndRolesFromClaims(claims map[string]interface{}) ([]string, []string, error) {
|
||||
var groups []string
|
||||
var roles []string
|
||||
|
||||
// Extract groups using configurable claim name (defaults to "groups")
|
||||
if groupsClaim, exists := claims[t.groupClaimName]; exists {
|
||||
groupsSlice, ok := groupsClaim.([]interface{})
|
||||
if !ok {
|
||||
@@ -1271,7 +1279,6 @@ func (t *TraefikOidc) extractGroupsAndRoles(idToken string) ([]string, []string,
|
||||
}
|
||||
}
|
||||
|
||||
// Extract roles using configurable claim name (defaults to "roles")
|
||||
if rolesClaim, exists := claims[t.roleClaimName]; exists {
|
||||
rolesSlice, ok := rolesClaim.([]interface{})
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user