From 69d55ada6c695c47f764285ca9e94736e50a8103 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Mon, 3 Aug 2026 22:19:27 +0200 Subject: [PATCH] fix: explicitly set SameSite to Lax for cookies --- backend/internal/utils/cookie/add_cookie.go | 16 +++++--- tests/specs/oidc.spec.ts | 45 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/backend/internal/utils/cookie/add_cookie.go b/backend/internal/utils/cookie/add_cookie.go index e865d15f..247ddbd1 100644 --- a/backend/internal/utils/cookie/add_cookie.go +++ b/backend/internal/utils/cookie/add_cookie.go @@ -1,28 +1,34 @@ package cookie import ( + "net/http" "time" "github.com/gin-gonic/gin" ) func AddAccessTokenCookie(c *gin.Context, maxAgeInSeconds int, token string) { - c.SetCookie(AccessTokenCookieName, token, maxAgeInSeconds, "/", "", true, true) + addCookie(c, AccessTokenCookieName, token, maxAgeInSeconds, "/") } func AddSessionIdCookie(c *gin.Context, maxAgeInSeconds int, sessionID string) { - c.SetCookie(SessionIdCookieName, sessionID, maxAgeInSeconds, "/", "", true, true) + addCookie(c, SessionIdCookieName, sessionID, maxAgeInSeconds, "/") } func AddDeviceTokenCookie(c *gin.Context, deviceToken string) { - c.SetCookie(DeviceTokenCookieName, deviceToken, int(15*time.Minute.Seconds()), "/api/one-time-access-token", "", true, true) + addCookie(c, DeviceTokenCookieName, deviceToken, int(15*time.Minute.Seconds()), "/api/one-time-access-token") } func AddDeviceLoginTokenCookie(c *gin.Context, requestID, deviceToken string) { path := "/api/device-login/requests/" + requestID + "/exchange" - c.SetCookie(DeviceLoginTokenCookieName, deviceToken, int(15*time.Minute.Seconds()), path, "", true, true) + addCookie(c, DeviceLoginTokenCookieName, deviceToken, int(15*time.Minute.Seconds()), path) } func AddReauthenticationTokenCookie(c *gin.Context, reauthenticationToken string) { - c.SetCookie(ReauthenticationTokenCookieName, reauthenticationToken, int(3*time.Minute.Seconds()), "/", "", true, true) + addCookie(c, ReauthenticationTokenCookieName, reauthenticationToken, int(3*time.Minute.Seconds()), "/") +} + +func addCookie(c *gin.Context, name, value string, maxAge int, path string) { + c.SetSameSite(http.SameSiteLaxMode) + c.SetCookie(name, value, maxAge, path, "", true, true) } diff --git a/tests/specs/oidc.spec.ts b/tests/specs/oidc.spec.ts index 79672638..a75c9e32 100644 --- a/tests/specs/oidc.spec.ts +++ b/tests/specs/oidc.spec.ts @@ -32,6 +32,51 @@ test('Authorize existing client', async ({ page }) => { ); }); +test('Authorize existing client with POST while signed in', async ({ page }, testInfo) => { + const oidcClient = oidcClients.nextcloud; + const formFields = Array.from(createUrlParams(oidcClient)); + const authorizeURL = new URL('/authorize', testInfo.project.use.baseURL).toString(); + const clientPageURL = new URL('/post-authorize-test', oidcClient.callbackUrl).toString(); + + await page.route(clientPageURL, async (route) => { + await route.fulfill({ status: 200, contentType: 'text/html', body: '' }); + }); + await page.goto(clientPageURL); + + const authorizeRequestPromise = page.waitForRequest( + (request) => request.method() === 'POST' && new URL(request.url()).pathname === '/authorize' + ); + await expectCallbackRedirect(page, oidcClient.callbackUrl, async () => { + // Create and submit a form with the OIDC parameters to simulate a POST request to the /authorize endpoint + await page.evaluate( + ({ fields, target }) => { + const doc = (globalThis as any).document; + const form = doc.createElement('form'); + form.method = 'POST'; + form.action = target; + + for (const [name, value] of fields) { + const input = doc.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + form.append(input); + } + + doc.body.append(form); + form.submit(); + }, + { fields: formFields, target: authorizeURL } + ); + + // Since the access token isn't sent with the POST request because of SameSite cookie restrictions, + // /authorize thinks the user is not signed in and shows the login page. + // The interaction page has the access token so the user just needs to click "Sign in" to continue the flow. + await page.getByRole('button', { name: 'Sign in' }).click(); + }); + await authorizeRequestPromise; +}); + test('Authorize existing client while not signed in', async ({ page }) => { const oidcClient = oidcClients.nextcloud; const urlParams = createUrlParams(oidcClient);