The SSE and WebSocket bypass does not check the token expiry. A session stays usable for streaming until the session cookie expires, also after the IdP disables the user. The bypass now forwards the configured headers (#162), so this gap also gives the proxy secret to such a session. Add the streamingRefresh option. The default is false. When it is true and the access token has expired, the bypass refreshes the session. It uses the ID token expiry if the access token is opaque. If the IdP rejects the refresh with invalid_grant, the request gets 401. If the IdP cannot be reached, the request is forwarded as before. The bypass waits for the full coordinated refresh. A shorter wait can drop a rotated refresh token, and the next refresh then logs the user out. applyBypassUserHeaders now takes the ResponseWriter, so a refresh can write the new session cookie. Add a yaegi check for the invalid_grant path through the real token endpoint and RefreshCoordinator. Claude-Session: https://claude.ai/code/session_01G1d1n1BrHHHw8dBUkbiBPC
8.0 KiB
Changelog
All notable changes to this project are documented in this file.
The format follows Keep a Changelog.
[Unreleased]
Added
-
allowUnauthenticatedPreflight(defaultfalse). EveryOPTIONSrequest needs authentication by default, as in the last release. Set the option totrueto let a genuine CORS preflight reach the backend without a session. Only anOPTIONSrequest that carries bothOriginandAccess-Control-Request-Methodbypasses authentication, and the plugin discards the backend's response body for it, because a browser never reads a preflight body (settings.go,middleware.go, FIX-02). See the option table in CONFIGURATION.md. -
streamingRefresh(defaultfalse). SSE and WebSocket requests do not check the token expiry, so a session stays usable for streaming until the session cookie expires. Set the option totrueto refresh an expired token on these requests. If the IdP rejects the refresh withinvalid_grant, the request gets401. If the IdP cannot be reached, the request is forwarded as before. The request waits for the full refresh, so a rotated refresh token is never lost (middleware.go).
Changed
-
Opaque bearer tokens when all three bearer flags are set. A deployment that sets
enableBearerAuth,requireTokenIntrospectionandallowOpaqueTokensnow accepts opaque bearer tokens after RFC 7662 introspection. The last release rejected every non-JWT bearer token. The plugin binds an accepted opaque token to the client throughclient_idoraud. It also applies the identifier, token-age, logout, revocation andnbfchecks (bearer_auth.go). -
Sessions created before this upgrade can omit
post_logout_redirect_urion their first logout. The plugin now builds that parameter only from the redirect-URI origin recorded at login, never from the logout request's host. Older sessions have no recorded origin, so those users stay on the IdP's logout page once (helpers.go). -
Shutdown waits up to 5 seconds for a token refresh in progress. On shutdown, for example a Traefik reload, the refresh coordinator lets a refresh that already reached the IdP deliver its tokens, then returns (
shutdownRefreshDrainTimeout,refresh_coordinator.go). A refresh that has not started does not start after shutdown. -
A panic after the request reaches the backend now aborts the connection. The plugin re-panics it, for example the reverse proxy's
http.ErrAbortHandlerwhen an upstream drops the connection mid-body. Go's HTTP server then aborts the connection. Before, the plugin recovered the panic, so a truncated response could reach the client as a complete 200. A panic before the request reaches the backend still returns 500 (middleware.go). -
private_key_jwt now rejects RSA signing keys under 2048 bits.
validateAlgKeyMatchstopsNew()with an error when a configuredprivate_key_jwtRS*/PS* key is smaller than 2048 bits (RFC 7518 §3.3). A smaller key signs locally, but a conformant IdP rejects it at token exchange withinvalid_client, so the plugin now fails at construction instead of at every runtime exchange. There is no override: deployments using an RSA key under 2048 bits must generate a 2048-bit-or-larger key before upgrading. The rejection returns a matchableErrRSAKeyTooSmallsentinel (client_assertion.go). -
A discovered
http://endpoint is now dropped whenproviderURLishttps://.validateDiscoveredEndpointrefuses to accept an authorization, token, jwks_uri, revocation, end_session, introspection, or registration endpoint served over plaintext HTTP when the operator configured an HTTPS provider (R146): using it as-is would send a client secret or token over an unauthenticated channel. There is no configuration flag to disable this check. A dropped endpoint logs anERRORline; a droppedtoken,jwks_uri, orauthorizationendpoint — the three every login needs — additionally logs aSECURITY:-prefixed line naming the endpoint, the dropped URL, and the fact that there is no override, so the failure is diagnosable instead of surfacing only as a broken login. See Discovered Endpoint Validation (url_helpers.go,main.go). -
Blacklist entries in Redis use the
blacklist:key prefix. The blacklist cache now has its own cache type (CacheTypeBlacklist), so its Redis keys move from the sharedtoken:prefix toblacklist:(R128). A cached token entry and a revocation marker for the same raw token can no longer overwrite each other. For one release, a blacklist miss also reads the legacytoken:<key>entry and treats it as revoked only when the stored value is booleantrue(checkLegacyBlacklistMarkerinuniversal_cache.go, FIX-16). The fallback works in one direction only: during a rolling deploy, a replica that runs the previous version does not see revocations that an upgraded replica writes underblacklist:. A blacklist miss checks the legacytoken:entry at most once per key in each 30 seconds, so the fallback adds little Redis traffic. -
Claim validation accepts four previously-rejected or silently-dropped claim shapes. Each change below is intentional, matches a production change, and is pinned by an existing regression test.
iat(issued-at) is optional injwt.Verify, per RFC 7519 §4.1.6. A token that omits it is validated oniss/aud/exp/nbfalone, instead of being rejected (jwt.go, R126). The lenient-audience access-token path (accessTokenUnexpiredintoken_validation_rs.go) applies the same rule, because both paths callverifyTimeClaims(jwt.go, FIX-27). Bearer-token auth still rejects a token with noiat: it returns"missing iat claim"(enforceIatAgeinbearer_auth.go), becausemaxTokenAgeSecondsalways applies —0falls back to a 24h default (main.go).- A numeric
sub(bearer-auth identifier) claim is stringified instead of rejected, so an IdP that emits a numeric subject still authenticates (bearer_auth.goresolveBearerIdentifier, R102). - A
groups/rolesclaim that is neither a string nor an array (for example a bare number ornull) is treated as an empty list instead of failing claim extraction outright, so a malformed claim on one side does not suppress a valid claim on the other (token_manager.goextractGroupsAndRolesFromClaims, R96). - A numeric element inside a
groups/rolesarray is stringified into the list instead of being silently dropped, so a numeric group ID survives extraction and can match anallowedRolesAndGroupsentry (token_manager.gostringListFromClaim,utilities.goclaimScalarString, R105).
Fixed
- The opaque bearer path rejects a token that
RevokeTokenadded to the blacklist, for example at logout. It also rejects an opaque token whosenbfis in the future (bearer_auth.go). - The backchannel-logout replay check treats a Redis error reply such as
READONLY,OOMorMISCONFas a failure, not as an unknown outcome. A replayed logout token is now rejected while Redis refuses writes (internal/cache/backends/redis.go). - After a failed cache write, a replica no longer ignores a newer value that
another replica wrote, such as a later logout. The preference for the local
value expires after 5 seconds (
backendStaleMarkTTL,universal_cache.go). - The token circuit breaker treats HTTP 408 from the token endpoint as a
transient failure, the same as 429 (
error_recovery.go). - With
allowUnauthenticatedPreflightenabled, a preflight response no longer carries a staleContent-Lengthwhen the backend writes a body without callingWriteHeader(middleware.go). - A dropped
http://revocation, end-session or introspection endpoint logs theSECURITY:line when its override (revocationURL,oidcEndSessionURLorintrospectionURL) is not configured (main.go).