fix: persist session cookie to disk to prevent PWA logout (#23746)

This commit is contained in:
Kyle Carberry
2026-04-01 09:54:59 -04:00
committed by GitHub
parent 12f87acad6
commit 8c8b307b97
2 changed files with 64 additions and 0 deletions
+15
View File
@@ -582,5 +582,20 @@ func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (*
Value: sessionToken,
Path: "/",
HttpOnly: true,
// MaxAge is set so the browser persists the cookie to disk rather
// than keeping it in memory as a session cookie. Standalone PWAs
// (display: standalone) run in their own browser process, and
// mobile OSes kill that process when the app is swiped away —
// deleting in-memory cookies and forcing an unexpected login.
//
// We use a long static value (1 year) instead of the key's
// LifetimeSeconds because the server refreshes the key's
// ExpiresAt on activity but does not re-set the cookie. Tying
// MaxAge to the key lifetime would cause the cookie to expire
// client-side even when the server-side key is still valid.
//
// Security is not affected: the server validates ExpiresAt on
// every request regardless of the cookie's MaxAge.
MaxAge: int((365 * 24 * time.Hour).Seconds()),
}), &newkey, nil
}
+49
View File
@@ -394,6 +394,55 @@ func TestSessionExpiry(t *testing.T) {
}
}
// TestSessionCookieMaxAge verifies that the session cookie is a persistent
// cookie (has MaxAge set) rather than a session cookie. Standalone PWAs
// run in their own browser process and mobile OSes purge in-memory
// (session) cookies when that process is killed, so the cookie must be
// persisted to disk.
func TestSessionCookieMaxAge(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client := coderdtest.New(t, nil)
// Create the first user (password-based login).
req := codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "SomeSecurePassword!",
}
_, err := client.CreateFirstUser(ctx, req)
require.NoError(t, err)
// Login via the raw HTTP endpoint so we can inspect the Set-Cookie header.
loginURL, err := client.URL.Parse("/api/v2/users/login")
require.NoError(t, err)
res, err := client.Request(ctx, http.MethodPost, loginURL.String(), codersdk.LoginWithPasswordRequest{
Email: req.Email,
Password: req.Password,
})
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
oneYear := int((365 * 24 * time.Hour).Seconds())
var found bool
for _, cookie := range res.Cookies() {
if cookie.Name == codersdk.SessionTokenCookie {
// MaxAge should be set to a long value so the browser
// persists the cookie to disk. The server handles real
// expiry via the API key's ExpiresAt field.
require.Equal(t, oneYear, cookie.MaxAge,
"Session cookie MaxAge should be set to 1 year for disk persistence")
found = true
}
}
require.True(t, found, "session cookie should be present in login response")
}
func TestAPIKey_OK(t *testing.T) {
t.Parallel()