diff --git a/.changeset/secure-token-cookies-over-https.md b/.changeset/secure-token-cookies-over-https.md new file mode 100644 index 00000000..672b22db --- /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 352fe9b5..d3d1aa2e 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 04c42fe7..e8984253 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 410309de..31a92ca8 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 00000000..26aace8f --- /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 00000000..91fee328 --- /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' +}