Skip to content

feat(gdpr): HttpOnly author-token cookie (PR3 of #6701)#7548

Open
JohnMcLear wants to merge 10 commits intodevelopfrom
feat-gdpr-anon-identity
Open

feat(gdpr): HttpOnly author-token cookie (PR3 of #6701)#7548
JohnMcLear wants to merge 10 commits intodevelopfrom
feat-gdpr-anon-identity

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

  • Author-token cookie is now minted and set by the server on the pad route as HttpOnly; Secure (on HTTPS); SameSite=Lax (or None when cross-site embedded).
  • Browser JavaScript no longer reads, writes, or sends the token; the token field is dropped from the CLIENT_READY message.
  • handleClientReady parses the token from the socket.io handshake Cookie header (socket.io doesn't run cookie-parser). Legacy message.token is honoured for one release with a one-time WARN per session.
  • No IP-based identity fallback (already true; documented in cookies.md pointer to privacy.md — see PR2 feat(gdpr): IP/privacy audit (PR2 of #6701) #7547).

Part of the GDPR work tracked in #6701. PR1 #7546 landed deletion controls; PR2 #7547 landed the IP/privacy audit. Remaining PR4 (cookie banner) and PR5 (author erasure) stay in follow-ups.

Design spec: docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md
Implementation plan: docs/superpowers/plans/2026-04-19-gdpr-pr3-anon-identity.md

Test plan

  • pnpm --filter ep_etherpad-lite run ts-check
  • ensureAuthorTokenCookie unit tests (5 cases: mint / reuse / Secure / SameSite=None when cross-site / invalid-existing)
  • authorTokenCookie integration tests (HttpOnly + reuse) — 2 passing
  • Playwright: HttpOnly attribute visible only via browser API, authorID stable across reload, authorID differs in isolated second context — 3 passing
  • Regression: chat + enter specs pass unchanged

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

feat(gdpr): HttpOnly author-token cookie (PR3 of #6701)

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Server now mints and sets author-token cookie as HttpOnly; Secure; SameSite=Lax
• Browser JavaScript no longer reads, writes, or generates the author token
• Socket.io handshake reads cookie first; legacy message.token supported one release
• Comprehensive unit, integration, and Playwright tests added for cookie lifecycle
Diagram
flowchart LR
  Client["Browser Client"]
  Server["Express Server"]
  Socket["Socket.io Handshake"]
  DB["Token→Author DB"]
  
  Client -->|GET /p/:pad| Server
  Server -->|Set-Cookie: HttpOnly token| Client
  Client -->|Socket connect with cookie| Socket
  Socket -->|Read cookie from headers| Server
  Server -->|Lookup token| DB
  DB -->|Return authorID| Server
Loading

Grey Divider

File Changes

1. src/node/utils/ensureAuthorTokenCookie.ts ✨ Enhancement +35/-0

New helper to mint and set HttpOnly author-token cookie

src/node/utils/ensureAuthorTokenCookie.ts


2. src/node/hooks/express/specialpages.ts ✨ Enhancement +5/-0

Call ensureAuthorTokenCookie on pad and timeslider routes

src/node/hooks/express/specialpages.ts


3. src/node/handler/PadMessageHandler.ts ✨ Enhancement +22/-1

Read token from cookie first, fallback to message with deprecation warn

src/node/handler/PadMessageHandler.ts


View more (7)
4. src/static/js/pad.ts ✨ Enhancement +3/-6

Remove client-side token generation and cookie write logic

src/static/js/pad.ts


5. src/tests/backend/specs/ensureAuthorTokenCookie.ts 🧪 Tests +77/-0

Unit tests for ensureAuthorTokenCookie helper with five scenarios

src/tests/backend/specs/ensureAuthorTokenCookie.ts


6. src/tests/backend/specs/authorTokenCookie.ts 🧪 Tests +47/-0

Integration tests for HttpOnly cookie set and reuse behavior

src/tests/backend/specs/authorTokenCookie.ts


7. src/tests/frontend-new/specs/author_token_cookie.spec.ts 🧪 Tests +49/-0

Playwright tests for HttpOnly attribute, reload stability, context isolation

src/tests/frontend-new/specs/author_token_cookie.spec.ts


8. doc/cookies.md 📝 Documentation +1/-1

Update token cookie row to reflect HttpOnly server-set behavior

doc/cookies.md


9. docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md 📝 Documentation +181/-0

Design specification for GDPR PR3 anonymous identity hardening

docs/superpowers/specs/2026-04-19-gdpr-pr3-anon-identity-design.md


10. docs/superpowers/plans/2026-04-19-gdpr-pr3-anon-identity.md 📝 Documentation +587/-0

Detailed implementation plan with eight tasks and self-review checklist

docs/superpowers/plans/2026-04-19-gdpr-pr3-anon-identity.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. No flag for HttpOnly token 📘 Rule violation ☼ Reliability
Description
The new server-minted HttpOnly author-token cookie behavior is enabled unconditionally on pad
routes, with no feature flag or default-off toggle. This violates the requirement that new features
be gated to reduce rollout risk and allow safe disablement.
Code

src/node/hooks/express/specialpages.ts[191]

+        ensureAuthorTokenCookie(req, res, settings);
Evidence
PR Compliance ID 5 requires new features to be behind a feature flag and disabled by default. The
pad routes now always call ensureAuthorTokenCookie(req, res, settings) (no conditional), making
the new behavior always-on.

src/node/hooks/express/specialpages.ts[191-191]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new HttpOnly author-token cookie behavior is always enabled, but PR compliance requires new features to be behind a feature flag and disabled by default.
## Issue Context
`ensureAuthorTokenCookie()` is now called on all pad and timeslider routes with no conditional gating, changing behavior immediately for all deployments.
## Fix Focus Areas
- src/node/hooks/express/specialpages.ts[191-191]
- src/node/hooks/express/specialpages.ts[222-222]
- src/node/hooks/express/specialpages.ts[354-354]
- src/node/hooks/express/specialpages.ts[375-375]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Timeslider rewrites token cookie🐞 Bug ⛨ Security
Description
The timeslider client still reads and (re)sets the author token cookie from JavaScript and sends it
over socket messages, which can overwrite the new server-minted HttpOnly token cookie and keep the
legacy message.token path active. This defeats the primary security goal (token not
JS-readable/writable) for any user who opens the timeslider.
Code

src/node/hooks/express/specialpages.ts[R221-223]

     setRouteHandler("/p/:pad/timeslider", (req: any, res: any, next: Function) => {
+        ensureAuthorTokenCookie(req, res, settings);
       console.log("Reloading pad")
Evidence
The server now ensures the author token cookie is set on the timeslider route via
ensureAuthorTokenCookie(). However, the existing timeslider frontend still calls
Cookies.get()/Cookies.set() for ${cp}token and includes token in its socket message payload,
which is incompatible with an HttpOnly cookie and can downgrade/replace it with a JS-managed cookie
value.

src/node/hooks/express/specialpages.ts[221-223]
src/node/utils/ensureAuthorTokenCookie.ts[17-34]
src/static/js/timeslider.ts[52-58]
src/static/js/timeslider.ts[100-108]
src/static/js/pad.ts[190-193]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The timeslider frontend still generates/sets the author token cookie in JavaScript and sends `token` in socket messages, which undermines the new HttpOnly cookie model.
### Issue Context
The server now sets the author token as an HttpOnly cookie on `/p/:pad/timeslider`, but the timeslider client still treats the token as JS-managed state.
### Fix Focus Areas
- src/static/js/timeslider.ts[52-58]
- src/static/js/timeslider.ts[100-109]
- src/static/js/pad.ts[190-212]
### What to change
- Remove the `token = Cookies.get(...); if (token == null) Cookies.set(...);` block.
- Remove `token` from the socket message payload (similar to `src/static/js/pad.ts` removing `token` from CLIENT_READY).
- Ensure the server path works purely via the socket.io handshake cookie (no message.token dependence for timeslider clients).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. tokenTransfer breaks HttpOnly model🐞 Bug ⛨ Security
Description
The welcome page’s tokenTransfer POST reads the author token from document.cookie (now invisible due
to HttpOnly), causing invalid requests, and the tokenTransfer GET endpoint re-sets the token cookie
without HttpOnly, undoing the hardening. This both breaks the feature and reintroduces a JS-readable
author token cookie.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R26-33]

+  const token = padutils.generateAuthorToken();
+  res.cookie(cookieName, token, {
+    httpOnly: true,
+    secure: Boolean(req.secure),
+    sameSite: isCrossSiteEmbed(req) ? 'none' : 'lax',
+    maxAge: 60 * 24 * 60 * 60 * 1000, // 60 days — matches the pre-PR3 client default
+    path: '/',
+  });
Evidence
ensureAuthorTokenCookie sets the author token cookie with httpOnly:true, so welcome.ts cannot read
it from document.cookie to POST /tokenTransfer. Additionally, tokenTransfer.ts sets ${p}token via
res.cookie without httpOnly/sameSite/secure, which will create a non-HttpOnly author token cookie
and bypass the intended protection.

src/node/utils/ensureAuthorTokenCookie.ts[26-33]
src/static/js/welcome.ts[24-33]
src/node/hooks/express/tokenTransfer.ts[16-20]
src/node/hooks/express/tokenTransfer.ts[42-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The tokenTransfer flow is incompatible with an HttpOnly author token: it tries to read the token in the browser and it also writes a non-HttpOnly token cookie server-side.
### Issue Context
The author token is now intended to be server-minted and HttpOnly. Any feature that depends on reading/writing the token in browser JS will break or regress security.
### Fix Focus Areas
- src/static/js/welcome.ts[24-33]
- src/node/hooks/express/tokenTransfer.ts[16-20]
- src/node/hooks/express/tokenTransfer.ts[42-45]
- src/node/utils/ensureAuthorTokenCookie.ts[26-33]
### What to change
- Update `POST /tokenTransfer` to not require `token` from the request body. Instead, read the token from `req.cookies` (include prefixed + unprefixed fallback).
- When setting `${p}token` in `GET /tokenTransfer/:token`, set cookie options consistent with PR3 (httpOnly, sameSite, secure-on-https, path='/').
- Update `welcome.ts` to stop attempting to read `${cp}token` from `document.cookie` and stop sending it in the POST body.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. CLIENT_READY no longer sends token 📘 Rule violation ☼ Reliability
Description
The browser no longer includes token in the CLIENT_READY message, which can break
interoperability with older servers that still rely on message.token. This is a
backward-compatibility risk for the public client/server interface during mixed-version or
cached-asset scenarios.
Code

src/static/js/pad.ts[212]

-    token,
Evidence
PR Compliance ID 3 requires maintaining backward compatibility for public interfaces. The PR removes
the token field from the outgoing CLIENT_READY payload (client-side), while the server
previously read message.token for authentication state, indicating a protocol expectation in older
versions.

src/static/js/pad.ts[212-212]
src/node/handler/PadMessageHandler.ts[300-300]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The client no longer sends `token` in `CLIENT_READY`, which can break compatibility with older servers that still require `message.token`.
## Issue Context
This is a client/server protocol surface; mixed-version scenarios (cached client assets, staggered deployments) can cause new-client/old-server failures.
## Fix Focus Areas
- src/static/js/pad.ts[207-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Missing unprefixed token fallback 🐞 Bug ≡ Correctness
Description
The new cookie lookup logic only checks ${prefix}token, so installations that previously relied on
an unprefixed token cookie (or that changed cookie.prefix) will silently mint a new token and
change the anonymous author identity. This is inconsistent with other server paths that still fall
back to unprefixed cookies.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R20-24]

+  const prefix = settings.cookie?.prefix || '';
+  const cookieName = `${prefix}token`;
+  const existing = req.cookies?.[cookieName];
+  if (typeof existing === 'string' && padutils.isValidAuthorToken(existing)) {
+    return existing;
Evidence
ensureAuthorTokenCookie only checks req.cookies[cookieName] (prefixed name), and
PadMessageHandler’s handshake parsing also targets only the prefixed name. But other server code
(padaccess) explicitly falls back to unprefixed req.cookies.token, indicating compatibility
expectations that this PR’s new path doesn’t preserve.

src/node/utils/ensureAuthorTokenCookie.ts[20-24]
src/node/handler/PadMessageHandler.ts[300-306]
src/node/padaccess.ts[8-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Author token resolution in PR3 does not fall back to the unprefixed `token` cookie when `settings.cookie.prefix` is set, which can reset identity for existing users.
### Issue Context
Other code paths already use `${prefix}token || token` fallback.
### Fix Focus Areas
- src/node/utils/ensureAuthorTokenCookie.ts[20-24]
- src/node/handler/PadMessageHandler.ts[300-307]
- src/node/padaccess.ts[8-13]
### What to change
- In `ensureAuthorTokenCookie`, if `cookieName` is not `'token'`, also check `req.cookies.token` as a fallback.
- If the fallback is used and valid, consider setting the prefixed cookie (HttpOnly) to the same value to migrate.
- In `PadMessageHandler`, if the prefixed cookie is absent, also try parsing `token=` from the handshake Cookie header (legacy/migration support).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Invalid SameSite=None on HTTP 🐞 Bug ☼ Reliability
Description
ensureAuthorTokenCookie can set SameSite=None when Sec-Fetch-Site=cross-site even if req.secure is
false, producing a cookie configuration that modern browsers reject and drop. This can break
embedded/cross-site usage on non-HTTPS or misconfigured TLS termination.
Code

src/node/utils/ensureAuthorTokenCookie.ts[R27-32]

+  res.cookie(cookieName, token, {
+    httpOnly: true,
+    secure: Boolean(req.secure),
+    sameSite: isCrossSiteEmbed(req) ? 'none' : 'lax',
+    maxAge: 60 * 24 * 60 * 60 * 1000, // 60 days — matches the pre-PR3 client default
+    path: '/',
Evidence
The helper sets sameSite: 'none' solely based on sec-fetch-site while secure is tied to
req.secure. The shared cookie code documents that SameSite=None does not work unless Secure is
true, so the helper can emit a cookie that will be ignored by browsers.

src/node/utils/ensureAuthorTokenCookie.ts[27-32]
src/static/js/pad_utils.ts[506-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The server may emit `SameSite=None` without `Secure`, which browsers reject.
### Issue Context
Cross-site embeds require `SameSite=None`, but only when `Secure=true`.
### Fix Focus Areas
- src/node/utils/ensureAuthorTokenCookie.ts[5-33]
- src/tests/backend/specs/ensureAuthorTokenCookie.ts[53-63]
### What to change
- Only set `sameSite: 'none'` if `req.secure` is true; otherwise fall back to `'lax'`.
- Add/adjust a unit test for the `cross-site + insecure` case to assert it does **not** emit `SameSite=None` without Secure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
7. Handshake cookie decode can throw 🐞 Bug ☼ Reliability
Description
PadMessageHandler uses decodeURIComponent() on the raw cookie value without guarding against
malformed percent-encoding, which can throw and fail the CLIENT_READY processing for that socket. A
single bad Cookie header can reliably trigger errors and disconnects.
Code

src/node/handler/PadMessageHandler.ts[R301-306]

+    const cookieHeader: string = socket.request?.headers?.cookie || '';
+    const cookieName = `${cookiePrefix}token`;
+    const cookieMatch = cookieHeader.split(/;\s*/).find(
+        (c) => c.split('=')[0] === cookieName);
+    const cookieToken = cookieMatch ? decodeURIComponent(cookieMatch.split('=').slice(1).join('=')) : null;
+    const legacyToken = typeof message.token === 'string' ? message.token : null;
Evidence
The code unconditionally calls decodeURIComponent() if it finds a matching cookie name.
decodeURIComponent throws on invalid percent-encoding, and SocketIORouter treats thrown errors as
message handling failures for that connection.

src/node/handler/PadMessageHandler.ts[301-306]
src/node/handler/SocketIORouter.ts[79-91]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Malformed cookie values can cause decodeURIComponent() to throw and break CLIENT_READY handling.
### Issue Context
Cookie values for the author token are expected to be base64url and do not need percent-decoding in the normal case.
### Fix Focus Areas
- src/node/handler/PadMessageHandler.ts[301-307]
### What to change
- Avoid decodeURIComponent entirely (treat the cookie value as opaque), or
- Wrap decodeURIComponent in try/catch and treat failures as `null` (optionally log at debug).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

8. CLIENT_READY types still require token 🐞 Bug ⚙ Maintainability
Description
The SocketIO message type definitions still require token in CLIENT_READY even though pad.ts no
longer sends it, making the type contracts misleading for future changes and refactors. This can
cause incorrect assumptions elsewhere (especially around timeslider / other clients).
Code

src/static/js/pad.ts[R207-211]

   type: 'CLIENT_READY',
   padId,
   sessionID: Cookies.get(`${cp}sessionID`) || Cookies.get('sessionID'),
-    token,
   userInfo,
 };
Evidence
pad.ts now constructs CLIENT_READY without a token field, but the shared type definition still
defines ClientReadyMessage.token: string, implying all CLIENT_READY messages must include token.

src/static/js/pad.ts[205-211]
src/static/js/types/SocketIOMessage.ts[180-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Type definitions still model CLIENT_READY as always including `token`.
### Issue Context
PR3 removes `token` from the main pad client; other clients should converge on the handshake-cookie model.
### Fix Focus Areas
- src/static/js/types/SocketIOMessage.ts[180-189]
- src/static/js/pad.ts[205-211]
### What to change
- Make `token` optional (or remove it) in the CLIENT_READY message type(s).
- If legacy token support remains for one release, model it as `token?: string` and document deprecation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo



setRouteHandler("/p/:pad", (req: any, res: any, next: Function) => {
ensureAuthorTokenCookie(req, res, settings);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. No flag for httponly token 📘 Rule violation ☼ Reliability

The new server-minted HttpOnly author-token cookie behavior is enabled unconditionally on pad
routes, with no feature flag or default-off toggle. This violates the requirement that new features
be gated to reduce rollout risk and allow safe disablement.
Agent Prompt
## Issue description
The new HttpOnly author-token cookie behavior is always enabled, but PR compliance requires new features to be behind a feature flag and disabled by default.

## Issue Context
`ensureAuthorTokenCookie()` is now called on all pad and timeslider routes with no conditional gating, changing behavior immediately for all deployments.

## Fix Focus Areas
- src/node/hooks/express/specialpages.ts[191-191]
- src/node/hooks/express/specialpages.ts[222-222]
- src/node/hooks/express/specialpages.ts[354-354]
- src/node/hooks/express/specialpages.ts[375-375]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/hooks/express/specialpages.ts
Comment thread src/node/utils/ensureAuthorTokenCookie.ts
Qodo review:
- Timeslider still ran the pre-PR3 JS-cookie path: it read
  Cookies.get('${cp}token') (which HttpOnly hides), then generated a
  fresh plaintext token and overwrote the server's HttpOnly cookie with
  it, and sent token in every socket message. Strip the token read/
  write entirely from timeslider.ts and from the outgoing message
  shape; the server reads the cookie off the socket.io handshake just
  like on /p/:pad.
- tokenTransfer re-issued the author cookie without HttpOnly, undoing
  the hardening the first time a user transferred a session. Re-set
  it as HttpOnly + Secure (on HTTPS) + SameSite=Lax. Also stop
  trusting the body-supplied token on POST: read it off req.cookies
  server-side so the client never needs JS access to the token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant