feat(middleware): refresh expired sessions on SSE/WebSocket bypass

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
This commit is contained in:
2026-09-28 15:31:18 +01:00
parent c9844d0ff2
commit 937776516d
13 changed files with 333 additions and 9 deletions
+6
View File
@@ -139,6 +139,7 @@ Full reference in [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
| `disableReplayDetection` | `false` | Disable JTI cache. Use Redis instead for multi-replica. |
| `allowPrivateIPAddresses` | `false` | Permit private-IP `providerURL` (internal Keycloak, etc.). |
| `allowUnauthenticatedPreflight` | `false` | Bypass auth for a genuine CORS preflight (`OPTIONS` + `Origin` + `Access-Control-Request-Method`); the response body is still discarded. Default requires auth for every `OPTIONS` request, preflight or not. |
| `streamingRefresh` | `false` | Refresh an expired session on SSE/WebSocket requests. Reject the request when the IdP revokes the grant. |
| `minimalHeaders` | `false` | Reduce forwarded headers (mitigates HTTP 431). |
| `stripAuthCookies` | `false` | Strip OIDC cookies from backend hop (mitigates HTTP 431). |
| `caCertPath` / `caCertPEM` | none | Trust an internal CA for the provider's TLS. |
@@ -309,6 +310,11 @@ WebSocket upgrade. The middleware handles this automatically:
- Validation is cookie-only (no JWK fetch), so streaming keeps working during
brief IdP outages. Back-channel and front-channel logout still apply: a
session that the IdP logged out is rejected.
- By default, the token expiry is not checked, so a session stays usable
for streaming until the session cookie expires. Set `streamingRefresh: true`
to refresh an expired token on the request. If the IdP rejects the refresh
(`invalid_grant`), the request gets `401`. If the IdP cannot be reached,
the request is forwarded as before.
No configuration needed — this is implicit behavior.