Skip to main content

EP-001: Authentication & Access Control — Phase 1 Hardening

Phase 1 hardening sprint, 2026-04. All work committed to feature branches in hmp-auth-service and hmp-shared-packages. Not yet pushed.

This epic gained two structural improvements during Phase 1: WebSocket JWT validation became real (it was previously a TODO marker on the gateways) and a service-account JWT mechanism was introduced so backend services can authenticate to each other without forwarding user tokens.


C1 — WebSocket Gateway JWT Authentication​

Branch: feat/ws-jwt-auth (in hmp-housekeeping-service, hmp-pos-service, hmp-web-dashboard, hmp-mobile-apps)

What was fixed​

  • HousekeepingGateway and KitchenGateway now validate JWTs on handleConnection. Connections with missing/invalid/expired tokens are immediately disconnected.
  • Token extraction supports three sources: Socket.IO auth.token (mobile), query string (web), and Authorization header (server-to-server).
  • joinProperty and joinFloor/joinOutlet events verify the requested propertyId matches the JWT claim. Cross-property join attempts return an error and do not subscribe the socket to the target room.
  • Mobile clients in all four React Native apps (HK, manager, F&B, guest) updated to forward the current access token from the auth store on every reconnect.

Files touched​

  • hmp-housekeeping-service/src/gateways/housekeeping.gateway.ts (+ .spec.ts)
  • hmp-pos-service/src/kitchen/kitchen.gateway.ts (+ .spec.ts)
  • hmp-web-dashboard/src/hooks/useRealtimeUpdates.ts
  • hmp-mobile-apps/packages/shared-realtime/src/useRealtimeTasks.ts
  • hmp-mobile-apps/packages/shared-realtime/src/useRealtimeKitchen.ts

Tests​

22+ new tests across the gateway specs, all passing. Coverage matrix:

  • valid token → connection accepted
  • expired token → disconnected
  • malformed token → disconnected
  • missing token → disconnected
  • joinProperty with matching propertyId → joins room
  • joinProperty with mismatched propertyId → returns error, no join
  • joinFloor/joinOutlet with valid scope → joins narrower room
  • mobile hook reconnects with refreshed token

Open follow-ups​

  • HS256 only. Both gateways use process.env.JWT_SECRET directly. RS256 with per-service public keys is required before production. Tracked in project_phase2-phase3-scope.md.
  • No connection-level rate limiting. A misbehaving client could open thousands of sockets. Phase 2 should add a per-tenant connection cap.

C4a — Internal Service-Account JWT Mechanism​

Branch: feat/service-account-jwt (in hmp-shared-packages, hmp-reservation-service, hmp-infrastructure)

What was added​

  • JwtAuthGuard in @hmp/auth now distinguishes service-account tokens (scope: 'service', aud: 'hmp-internal') from user tokens (scope: 'user', aud: 'hmp-public'). Service tokens get req.user.type = 'service' and tenantId/propertyId from headers; user tokens get them from claims.
  • BaseServiceClient in @hmp/service-clients auto-injects Authorization: Bearer <INTERNAL_SERVICE_TOKEN> and forwards x-tenant-id, x-property-id, x-request-id from AsyncLocalStorage.
  • New script hmp-infrastructure/scripts/generate-service-token.sh for ops to mint and rotate service-account tokens.

Architecture documentation​

Full spec in architecture/internal-service-auth.md. Companion docs:

Tests​

18 new tests on JwtAuthGuard (token type discrimination, header forwarding, expiry, audience mismatch). All passing.

Open follow-ups​

  • HS256 → RS256 migration (same as C1 — shared secret blast radius).
  • Per-service scopes. Today all service tokens carry scope: 'service'. Phase 2 should introduce service:reservation, service:property, etc., for finer-grained revocation.
  • Token storage. Demo uses static env value; staging/prod should pull from External Secrets Operator → AWS Secrets Manager.

Summary​

StoryStatus beforeStatus afterNotes
US-001-001 (User Login)BuiltBuiltNo change
US-001-002 (JWT Refresh)BuiltBuiltNow coexists cleanly with service-account tokens
US-001-006 (RBAC)BuiltBuiltService tokens bypass RBAC; receivers must enforce ownership
US-001-010 (Brute force)BuiltBuiltNo change
(new) WebSocket authnot trackedshippedNew scope, see C1
(new) Service-account JWTnot trackedshippedNew scope, see C4a

No story status changes; the gains are structural rather than story-by-story. The two new capabilities should probably get their own US-XXX entries in a future registry pass.