Loading
Draft: Enforce auth session cookie lifetime on the server side
What does this MR do?
Enforces the gitlab-pages auth session cookie lifetime (auth-cookie-session-timeout, default 10 minutes) on the server side. Until now it was only limited client-side through Max-Age.
Closes #1184 (closed)
Root cause
- In
internal/auth/auth.go,New()created the store withsessions.NewCookieStore(keys[0], keys[1]), which kept gorilla/securecookie's default codecMaxAgeof 30 days. - In
internal/auth/session.go,getSessionFromStoreset onlysession.Options.MaxAge. That value goes into the browserMax-Ageattribute and is not used when the server validates the cookie. - As a result, a copied cookie kept working after GitLab logout until the OAuth access token expired (about 2 hours), not the intended 10 minutes.
Fix
- Codec MaxAge:
New()now callsstore.MaxAge(CookieSessionTimeout in seconds). The securecookie codecs then reject cookies whose signed timestamp is older than the timeout. A zero (or sub-second) timeout keeps the old behavior. Zero means "browser-session cookie", so the codec keeps its 30-day default and existing defaults don't change. - Re-save finding: The session is not re-saved on a normal authenticated request.
checkAuthenticationwith a valid token and a200frompages_accessdoesn't callSave(the one exception is a single save to clear the SSO re-auth marker). However, a session that holds a token can be re-saved with the token kept, and each re-save refreshes the securecookie timestamp. For example, requesting/auth?domain=<allowed domain>&state=xwith the cookie (handleProxyingAuth) saves the session with all its values before redirecting to GitLab. With only fix 1, that would turn the limit into a sliding window that is capped only by the OAuth token's own expiry. To prevent this:- The OAuth callback now stores
access_token_issued_atnext toaccess_token. This follows the existingstate_issued_atpattern and usesa.now(). getSessionFromStoredropsaccess_tokenonce it is older thanCookieSessionTimeout. Every consumer (checkTokenExists,GetTokenIfExists, the forbidden re-auth path) then treats the session as unauthenticated.destroySessionalso clears the issued-at value.
- The OAuth callback now stores
- Re-auth, no 500:
- When the codec rejects an expired cookie, the existing
checkSessionerror path runs: it saves a fresh empty session and redirects to the same URL. That request has no token, socheckTokenExistsredirects to the normal OAuth flow (/auth?domain=…&state=…and then GitLab authorize). - When only the issued-at check fails, the token is dropped and the request goes straight to the OAuth flow.
- When the codec rejects an expired cookie, the existing
Tradeoffs / notes
- Sessions created before this change have no
access_token_issued_at. For those, only the codecMaxAgeapplies (time since the cookie was last saved). I didn't reject them outright. During a rolling deploy, a callback handled by an old pod would issue a cookie without the key, and a new pod would keep rejecting it, which could cause a redirect loop. This gap only lasts as long as the pre-deploy OAuth tokens (about 2 hours). - There is no feature flag. The change only enforces the existing configured timeout.
- This is a bugfunctional defense-in-depth change, fixed in public as agreed with AppSec in the issue. It doesn't revoke the OAuth token on GitLab logout. It limits how long a copied cookie can be used to
CookieSessionTimeout.
Tests (internal/auth/auth_test.go)
TestCheckAuthenticationEnforcesCookieSessionTimeoutServerSide:- A fresh cookie (9 min old) is accepted.
- An 11-minute-old cookie is rejected without calling the API, is redirected to itself, and the next request redirects to the OAuth flow.
- A zero timeout keeps the default lifetime (a 24h-old cookie is still accepted).
- securecookie stamps cookies with the wall clock and has no hook to change it, so a test helper re-signs the cookie's
date|value|macpayload with a backdated timestamp. The "fresh" case also confirms that the re-signed cookie is valid.
TestCheckAuthenticationRejectsAccessTokenOlderThanCookieSessionTimeout: uses a freshly saved cookie, which is the re-save scenario, and controls time throughauth.now. The token is accepted 1s before the timeout and dropped at the timeout, with a redirect to OAuth.TestTryAuthenticateStoresAccessTokenIssuedAt: the callback records the issued-at value.
TODO
- Feature flag
- Added feature flag:
- This feature does not require a feature flag
- I added the
Changelogtrailer to the commits that need to be included in the changelog (e.g.Changelog: added) - I added unit tests or they are not required
- I added acceptance tests or they are not required
- I added documentation (or it's not required)
- I followed code review guidelines
- I followed Go Style guidelines