From 94612d0c8593ffd220f46e52d1f3aed2e4d23bb2 Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Sun, 4 Oct 2026 17:03:39 +0000 Subject: [PATCH] fix(auth): set token cookies Secure only over HTTPS Login on a production build served over plain HTTP (e.g. a LAN test server) never stored the access and refresh tokens: they were marked Secure because NODE_ENV is "production", and browsers drop Secure cookies set from HTTP pages, so /users/me answered 401. shouldUseSecureCookies() follows the page's protocol in the browser and keeps the NODE_ENV rule elsewhere; useLogin and refreshAccessToken use it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019P1oyApi4BLS8iLfMEGm8b --- .changeset/secure-token-cookies-over-https.md | 13 ++++++++++ .../modules/access/useLogin/index.ts | 5 ++-- packages/utils/functions/token/index.ts | 1 + .../token/refreshAccessToken/index.ts | 3 ++- .../__tests__/shouldUseSecureCookies.test.ts | 26 +++++++++++++++++++ .../token/shouldUseSecureCookies/index.ts | 14 ++++++++++ 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .changeset/secure-token-cookies-over-https.md create mode 100644 packages/utils/functions/token/shouldUseSecureCookies/__tests__/shouldUseSecureCookies.test.ts create mode 100644 packages/utils/functions/token/shouldUseSecureCookies/index.ts diff --git a/.changeset/secure-token-cookies-over-https.md b/.changeset/secure-token-cookies-over-https.md new file mode 100644 index 000000000..672b22db0 --- /dev/null +++ b/.changeset/secure-token-cookies-over-https.md @@ -0,0 +1,13 @@ +--- +"@baseapp-frontend/utils": patch +"@baseapp-frontend/authentication": patch +--- + +Token cookies are `Secure` only when the page is served over HTTPS. Keying the flag on +`NODE_ENV` broke login on production builds served over plain HTTP (a LAN or a staging host +without TLS): browsers drop `Secure` cookies set from HTTP pages, so the tokens were never +stored and `/users/me` answered 401. + +- New `shouldUseSecureCookies()` in `@baseapp-frontend/utils`: the page's protocol in the + browser, the previous `NODE_ENV` rule elsewhere. +- `useLogin` and `refreshAccessToken` use it. diff --git a/packages/authentication/modules/access/useLogin/index.ts b/packages/authentication/modules/access/useLogin/index.ts index 352fe9b57..d3d1aa2ea 100644 --- a/packages/authentication/modules/access/useLogin/index.ts +++ b/packages/authentication/modules/access/useLogin/index.ts @@ -8,6 +8,7 @@ import { decodeJWT, setFormApiErrors, setTokenAsync, + shouldUseSecureCookies, } from '@baseapp-frontend/utils' import { isMobilePlatform } from '@baseapp-frontend/utils/functions/os' @@ -80,10 +81,10 @@ const useLogin = ({ } await setTokenAsync(accessKeyName, response.access, { - secure: process.env.NODE_ENV === 'production', + secure: shouldUseSecureCookies(), }) await setTokenAsync(refreshKeyName, response.refresh, { - secure: process.env.NODE_ENV === 'production', + secure: shouldUseSecureCookies(), }) } diff --git a/packages/utils/functions/token/index.ts b/packages/utils/functions/token/index.ts index 04c42fe75..e8984253d 100644 --- a/packages/utils/functions/token/index.ts +++ b/packages/utils/functions/token/index.ts @@ -6,3 +6,4 @@ export * from './isUserTokenValid' export * from './refreshAccessToken' export * from './removeTokenAsync' export * from './setTokenAsync' +export * from './shouldUseSecureCookies' diff --git a/packages/utils/functions/token/refreshAccessToken/index.ts b/packages/utils/functions/token/refreshAccessToken/index.ts index 410309dea..31a92ca81 100644 --- a/packages/utils/functions/token/refreshAccessToken/index.ts +++ b/packages/utils/functions/token/refreshAccessToken/index.ts @@ -2,6 +2,7 @@ import { ACCESS_KEY_NAME, REFRESH_KEY_NAME } from '../../../constants/jwt' import { getAccessToken } from '../getAccessToken' import { removeTokenAsync } from '../removeTokenAsync' import { setTokenAsync } from '../setTokenAsync' +import { shouldUseSecureCookies } from '../shouldUseSecureCookies' import { RefreshAccessTokenParams } from './types' export const refreshAccessToken = async ({ @@ -13,7 +14,7 @@ export const refreshAccessToken = async ({ const accessToken = await getAccessToken(refreshToken) await setTokenAsync(accessKeyName, accessToken, { - secure: process.env.NODE_ENV === 'production', + secure: shouldUseSecureCookies(), }) return accessToken diff --git a/packages/utils/functions/token/shouldUseSecureCookies/__tests__/shouldUseSecureCookies.test.ts b/packages/utils/functions/token/shouldUseSecureCookies/__tests__/shouldUseSecureCookies.test.ts new file mode 100644 index 000000000..26aace8f1 --- /dev/null +++ b/packages/utils/functions/token/shouldUseSecureCookies/__tests__/shouldUseSecureCookies.test.ts @@ -0,0 +1,26 @@ +import { shouldUseSecureCookies } from '..' + +describe('shouldUseSecureCookies', () => { + const originalLocation = window.location + + const setProtocol = (protocol: string) => { + Object.defineProperty(window, 'location', { + configurable: true, + value: { ...originalLocation, protocol }, + }) + } + + afterEach(() => { + Object.defineProperty(window, 'location', { configurable: true, value: originalLocation }) + }) + + it('is secure on HTTPS pages', () => { + setProtocol('https:') + expect(shouldUseSecureCookies()).toBe(true) + }) + + it('is not secure on plain-HTTP pages, whatever NODE_ENV says', () => { + setProtocol('http:') + expect(shouldUseSecureCookies()).toBe(false) + }) +}) diff --git a/packages/utils/functions/token/shouldUseSecureCookies/index.ts b/packages/utils/functions/token/shouldUseSecureCookies/index.ts new file mode 100644 index 000000000..91fee3289 --- /dev/null +++ b/packages/utils/functions/token/shouldUseSecureCookies/index.ts @@ -0,0 +1,14 @@ +/** + * Whether token cookies should be `Secure`. + * + * Browsers drop `Secure` cookies set from plain-HTTP pages (except on localhost), so keying the + * flag on `NODE_ENV` breaks login on a production build served over HTTP, e.g. on a LAN or a + * staging host without TLS: the tokens are never stored and every request is anonymous. In the + * browser this follows the page's protocol; elsewhere (SSR, native) it keeps the `NODE_ENV` rule. + */ +export const shouldUseSecureCookies = (): boolean => { + if (typeof window !== 'undefined' && window.location?.protocol) { + return window.location.protocol === 'https:' + } + return process.env.NODE_ENV === 'production' +}