Authentication
The server uses two separate auth mechanisms — one for REST, one for WebSocket — backed by Clerk as the identity provider.
REST Authentication — ClerkAuthGuard
File: apps/server/src/auth/clerk-auth.guard.ts
Library: @clerk/backend (verifyToken)
How it works
- Reads
Authorization: Bearer <token>from the request header - Calls
verifyToken(token, { secretKey: CLERK_SECRET_KEY }) - Extracts
subclaim → setsrequest.user = { id: sub } - Returns
true— handler runs
Failure responses (all HTTP 401)
| Condition | Message |
|---|---|
No Bearer token | "Missing or invalid Authorization header" |
CLERK_SECRET_KEY not set | "Server auth not configured (Clerk)" |
Token verified but no sub | "Invalid token: missing sub" |
| Verification throws | "Invalid or expired token" |
Where it's applied
Applied per-controller via @UseGuards(ClerkAuthGuard) — not globally. Every authenticated controller must include the decorator explicitly.
| Controller | Endpoints |
|---|---|
UsersController | GET /users/me, PATCH /users/me |
MatchesController | GET /matches, GET /matches/:id, POST /matches/singleplayer, POST /matches/rematch |
FriendsController | All /friends endpoints |
ChallengesController | GET /challenges |
StatisticsController | GET /statistics, GET /statistics/time-series |
XpController | GET /xp/me |
@CurrentUser() decorator
Extracts the authenticated user from request.user inside a handler:
typescript
@Get('me')
async getMe(@CurrentUser() user: AuthUser) {
// user.id = Clerk user ID
}WebSocket Authentication — Identify Protocol
Socket.IO connections are not authenticated with a JWT. The design is:
- Client connects with no credentials
- Client immediately emits
identify { playerId, name, expoPushToken? } MatchmakingGatewaymapssocketId → userIdinPresenceService- All subsequent socket messages resolve identity via
presenceService.getUserId(socket.id)
Trust model
| Aspect | Detail |
|---|---|
| Connection | Open — no token required |
| Identity | Client provides playerId — server takes it at face value |
| Game messages | Include playerId in payload; some (not all) are cross-checked against presence |
| Reconnect | Client re-sends identify; old socket entry is overwritten |
No JWT is verified on the socket. This is an intentional trade-off for low-latency real-time gameplay. The Clerk JWT is only used for REST calls on the client side.
Admin Key Guard
File: apps/server/src/game-config/game-config.controller.ts (inline)
A simple key-match guard for admin config endpoints:
- Reads
ADMIN_KEYenv var - Accepts via
x-admin-keyheader oradmin_keyquery param - Throws HTTP 401 on mismatch
Applied to: GET /config/full, PUT /config, POST /config/reload, GET /config/rewards, PUT /config/rewards.GET /config is public (no guard).
Global Configuration
Rate limiting (via ThrottlerGuard, registered as APP_GUARD):
| Tier | Limit |
|---|---|
| Short | 30 requests / 10 s per IP |
| Medium | 100 requests / 60 s per IP |
No global auth guard. Auth is opt-in per controller.
CORS: origin: true, credentials: true (all origins allowed).
Global pipe: ValidationPipe with transform: true, whitelist: true.
Summary
| Layer | Mechanism | JWT used? |
|---|---|---|
| REST endpoints | ClerkAuthGuard per-controller | Yes — Clerk JWT |
| WebSocket connection | None | No |
| WebSocket messages | Presence map (socketId → userId) | No |
| Admin config endpoints | ADMIN_KEY env var | No |