Skip to content

impr(auth): optional authentication for public endpoints with user data (@fehmer) - #8566

Draft
fehmer wants to merge 5 commits into
masterfrom
feature/skip-auth-on-public
Draft

fehmer wants to merge 5 commits into
masterfrom
feature/skip-auth-on-public

Conversation

@fehmer

@fehmer fehmer commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Summary

RequestAuthenticationOptions.isPublic now has three access modes:

- isPublic?: boolean
+ isPublic?: boolean | "optionalUser"
authenticateTsRestRequest
+  if isPublic === true       → skip, token None  (auth header ignored)
   if isGithubWebhook         → verify signature
   if authorization header    → verify token (also covers optionalUser + token)
   if isPublic or dev-public  → skip, token None  (no header: optionalUser / isPublicOnDev)
   else                       → 401
Mode Token sent No token
isPublic: true ignored allowed
isPublic: "optionalUser" verified, user used for user-specific data allowed
default required 401

Endpoints: 7 stay true (/configuration, /psas, /quotes/isSubmissionEnabled, /users/:uidOrName/profile, /users/forgotPasswordEmail, /public/speedHistogram, /public/typingStats — none read the token). 4 get optionalUser because they use the authenticated uid for optional user-specific data: GET /users/checkName/:name (own-name exclusion) and GET /leaderboards + /daily + /xp/weekly (friendsOnly filter) — these keep the previous behaviour, no regression.

Changed behaviour

Only the 7 isPublic: true endpoints change behaviour (they previously verified a token when one was sent). The 4 optionalUser endpoints, the isPublicOnDev endpoints and webhooks behave exactly as before.

Authentication — on these 7 endpoints a sent Bearer/ApeKey header is now ignored instead of verified: a broken/expired/revoked token can no longer fail the request (no more 401/404/503 from Firebase on public data).

User-based rate limiting — limiters key by uid when authenticated, else by IP. Since the user is no longer decoded, logged-in requests now count against the per-IP bucket:

Endpoint Limiter Logged-in key before → after
GET /psas 60/min per-uid → per-IP
GET /quotes/isSubmissionEnabled 60/min per-uid → per-IP
GET /users/:uidOrName/profile 100/h per-uid → per-IP
POST /users/forgotPasswordEmail 1/min per-uid → per-IP
GET /public/speedHistogram 60/min (shared) per-uid → per-IP
GET /public/typingStats 60/min (shared) per-uid → per-IP
GET /configuration none (root limiter is IP-only) unchanged

Related minor changes on these 7: ApeKey requests now hit the normal limiter (per-IP, 429 on exceed) instead of the ApeKey limiter (defaultApeRateLimit, 30/min, 479 on exceed); the auth prometheus metrics record None for logged-in users; invalid tokens no longer record auth failures.

OpenAPI docs

OpenAPI cannot express optional authentication (a listed scheme is required, security: [] means none). optionalUser endpoints therefore keep security: [] plus:

  • x-optional-auth: [{ BearerAuth: [...] }, ...] vendor extension listing the schemes accepted when provided
  • a description note: Authentication is optional. If a valid token is provided, user-specific data is included.

Public docs (gen-docs, filtered on x-public: yes): verified to contain all 7 isPublic: true and all 4 optionalUser endpoints; isPublic: true endpoints and permission-gated endpoints are unchanged.

Evidence

  • Before: public endpoints verified tokens when sent (old tests asserted Bearer/ApeKey decode on isPublic: true endpoints)
  • After: backend 556/556, contracts 1/1, frontend 1752/1752 unit tests. auth.spec.ts covers both cases per mode (token → verified / no token → allowed / invalid token → 401 / apeKey). Controller specs cover friendsOnly and checkName for authenticated and unauthenticated requests

Merge Danger

Door: two-way — reverting restores the previous boolean flag; no data, migration, or API-shape changes

Blast radius: the 7 isPublic: true endpoints — biggest practical impact is userForgotPasswordEmail (1/min) now bucketed per-IP, so users sharing an IP (NAT/office) share that budget. optionalUser endpoints unchanged.

@monkeytypegeorge monkeytypegeorge added the backend Server stuff label Nov 11, 2026
@fehmer fehmer changed the title impr(auth): skip authentication on public endpoints (@fehmer) impr(auth): optional authentication for public endpoints with user data (@fehmer) Nov 11, 2026
@monkeytypegeorge monkeytypegeorge added the packages Changes in local packages label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server stuff packages Changes in local packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants