Skip to content

🐛 Real authentication and API hardening #22

@fusion94

Description

@fusion94

Summary

The current auth model has multiple critical gaps. The biggest: anyone who knows a username can rewrite that user's profile, photo, location, and birthday with no proof of identity. This single issue bundles four security fixes that are tightly related and should ship together.

Findings

1. /api/user/* mutation endpoints have no auth (privilege escalation)

  • PUT /api/user/:username (server/index.js:396) — change full name, location, marker color, birthday, display name. No auth check.
  • POST /api/user/:username/photo (server/index.js:508) — upload an avatar for any user.
  • DELETE /api/user/:username/photo (server/index.js:535) — delete any user's avatar.
  • GET /api/user/:username (server/index.js:383) — read full profile (including coordinates) of any user.

PUT /api/user/:username/password (server/index.js:429) is gated by the old-password check so it's less urgent, but should also require auth for defense-in-depth.

2. The x-username header is decorative

requireAdmin (server/index.js:162-181) and requireAuth (where used at all) trust a plaintext x-username header set by the client. Anyone can set the header to any value — including an admin's. The bcrypt password check at login produces no token; nothing carries the auth result forward.

3. No rate limits on public endpoints

/api/login, /api/register, /api/reset-password, /api/verify-reset-token (and /api/forgot-password once #20 lands) are unauthenticated and unthrottled. Credential stuffing and reset-token brute force are wide open.

4. CORS is wide open

app.use(cors()) (server/index.js:138) accepts any origin. With cookie-based auth this becomes a CSRF vector.

Proposed Approach

  1. Add a requireAuth middleware that validates a real session token (httpOnly cookie or Authorization: Bearer <jwt>) and apply it to all /api/user/:username/* routes plus everywhere x-username is currently trusted.
  2. Issue the token on POST /api/login. Verify on every request with a server secret read from env (SESSION_SECRET).
  3. Phase the migration: keep the legacy x-username header working for one release, then remove it.
  4. Add express-rate-limit (5 attempts / 15 min / IP) to all public endpoints.
  5. Restrict CORS to process.env.ALLOWED_ORIGINS?.split(','). Default to http://localhost:3000 for dev.
  6. Document SESSION_SECRET and ALLOWED_ORIGINS in .env.example.

Acceptance Criteria

  • PUT /api/user/:username, POST /api/user/:username/photo, DELETE /api/user/:username/photo reject requests where the authenticated user is not :username (or admin).
  • GET /api/user/:username requires auth.
  • Auth is carried by a signed token, not a plaintext header.
  • Login issues the token; logout invalidates it (or relies on short expiry + refresh).
  • Login, register, password-reset endpoints reject after 5 attempts in 15 minutes from the same IP.
  • CORS rejects requests from origins not in the allowlist.
  • Existing flows still work for the current admin user end-to-end.

Additional Context

Bundled because they share a single migration: introducing real auth is the prerequisite for tightening the other three. Splitting would require duplicate work.

Related: #19 (audit log will record auth events once this is in), #20 (Resend will use the same rate limiter).

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions