Gate calls Google userinfo on every API request instead of caching the session
Environment
- Gate version: 2026.2.0
- Spring Security: 6.5.11
- Spring Boot: 3.x (Spring Security 6 defaults apply)
- Spring Session: 3.5.7
- Session store: Redis
Summary
Gate calls Google's /oauth2/v3/userinfo on every Bearer-token request. No session is
created. No Set-Cookie header is returned. Each request repeats the full Google
round-trip (~100-200ms from the Gate pod).
Under Spring Security 5, sessions were saved implicitly. Spring Security 6 changed the
default to requireExplicitSave=true. The filter was not updated to save explicitly.
Browser OAuth2 login is not affected — OAuth2LoginAuthenticationFilter does save the
session. Only Bearer-token API clients are affected.
Impact
- Performance: ~100-200ms overhead on every bearer-token request (Google userinfo round-trip). Direct measurement shows ~100-120ms from a developer workstation.
- Reliability: If Google returns 503 or times out (5s default), Gate redirects to OAuth login (302). API clients receive HTML instead of JSON.
Behavior
Expected: Gate validates the Bearer token once, saves a session, returns a SESSION
cookie. Subsequent requests use the cookie. Google userinfo is not called again.
Actual: Gate validates the Bearer token, does not save a session, does not return a cookie. Every request calls Google userinfo.
Reproduction
TOKEN=$(gcloud auth print-access-token)
# No Set-Cookie in response
curl -s -D - -o /dev/null \
-H "Authorization: Bearer $TOKEN" \
'https://spinnaker-gate.CLUSTER/applications?pageSize=1' \
| grep -iE "set-cookie|HTTP/"
# Second request — same latency (no caching)
curl -s -o /dev/null -w "time=%{time_total}s\n" \
-H "Authorization: Bearer $TOKEN" \
'https://spinnaker-gate.CLUSTER/applications?pageSize=1'Root cause
ExternalAuthTokenFilter sets the authentication in the SecurityContext but does not
persist it:
// This is called — authentication is set for the current request
SecurityContextHolder.getContext().setAuthentication(authentication);
// This is NOT called — no session is created, no cookie is returned
securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);Under Spring Security 5, SecurityContextPersistenceFilter saved the context
automatically (requireExplicitSave=false). Under Spring Security 6,
SecurityContextHolderFilter requires explicit saves (requireExplicitSave=true).
OAuth2SsoConfig does not override this default.
Source: ExternalAuthTokenFilter.java — doFilterInternal() →
authenticateWithToken() → fetchUserInfo().
Timeline
| Date | Event | Session impact |
|---|---|---|
| Pre-2025 | Spring Security 5 (requireExplicitSave=false) |
Implicit save — Bearer-token sessions worked |
| Feb 2025 | PR #1871 — Groovy → Java conversion | No behavior change — implicit save still active |
| Sep 2025 | PR #7052 — Removed ExternalAuthTokenFilter | Bearer login broke |
| Nov 2025 | PR #7057 — Boot 2.7.18 → 3.0.13 | Spring Security 6 introduced — requireExplicitSave=true now default |
| Jan 2026 | PR #7392 — Reintroduced filter on Spring Security 6 codebase. Reviewer noted per-request overhead | Filter sets SecurityContext but omits saveContext() — regression introduced |
| Jun 2026 | PR #7682 — Boot 3.1.12 → 3.5.14 | Boot upgrade only — did not introduce Spring Security 6 |
PR #7392 introduced the current missing-save behavior on an already-Spring-Security-6 codebase (established by PR #7057). PR #7682 did not introduce Spring Security 6.
Suggested fix
Option A — Save explicitly in ExternalAuthTokenFilter. The filter would need a
SecurityContextRepository injected (it does not currently have one):
securityContextRepository.saveContext(
SecurityContextHolder.getContext(), request, response);Option B — Restore implicit save in OAuth2SsoConfig:
http.securityContext(context -> context.requireExplicitSave(false))Security consideration: Saving the context would restore the behavior used by Gate 2025.0.6. In that version, an established Gate session could outlive the Google Bearer token's expiry or revocation. Gate limited the session with a seven-day inactivity timeout and invalidated it if Fiat no longer had permissions for the principal. Topper also kept its cookie jar only for the life of one process. Gate did not revalidate the Google token after it loaded authentication from the session. This risk therefore existed in the previous session model; restoring session persistence does not introduce it.
Related issues
Source: spinnaker/spinnaker