Files
eatme/docs/architecture/auth-flow.md
Michal Pemcak fffcb73ea4 Initial commit: EatMe attendance tracker
Google-SSO PWA for bistro employee clock-in/out, admin employee
management, and stats with CSV export. Express + SQLite backend,
React + Zustand frontend in the light-mono-tui design language.
Multi-stage Dockerfile, compose.yaml for image-based deploys, nginx
reverse-proxy template, and an OKF documentation bundle in docs/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 18:41:11 +02:00

2.3 KiB

type, title, description, tags, timestamp
type title description tags timestamp
Architecture Overview Authentication & authorization Google ID-token verification, the session cookie, and the admin/employee role split.
architecture
auth
security
2026-08-12T00:00:00Z

Authentication & authorization

Sign-in

  1. The frontend loads the Google Identity Services script (https://accounts.google.com/gsi/client, in frontend/index.html) and renders the button in frontend/src/auth/GoogleButton.tsx.
  2. Google returns a signed ID token to the browser (credential). The frontend does not trust it — it POSTs it to POST /api/auth/google.
  3. The backend verifies the token server-side with google-auth-library (backend/src/auth/google.ts, verifyGoogleIdToken), checking the audience against GOOGLE_CLIENT_ID and requiring email_verified.

Role resolution

Two independent checks decide who gets in and as what role (backend/src/routes/auth.ts):

  • Admin: the verified email is in the ADMIN_EMAILS env var (comma-separated, lowercased — backend/src/env.ts). Admins do not need a row in the employees table.
  • Employee: the verified email matches an active row in employees.

If neither matches, login is rejected with 403 ("This account is not registered as an EatMe employee"). This is how the employee whitelist is enforced — there's no self-service signup.

Session

On success the backend signs its own JWT (backend/src/auth/session.ts, issueSession) containing { email, name, role, employeeId }, and sets it as an httpOnly, SameSite=Lax cookie (eatme_session, 12h TTL). All subsequent /api/* requests carry that cookie (frontend/src/api/client.ts sends credentials: 'include'); the backend never re-derives role from the DB on every request — it trusts the signed cookie until it expires.

backend/src/auth/middleware.ts provides three guards used by the routers:

  • requireAuth — valid session cookie present
  • requireAdminrole === 'admin'
  • requireEmployeeemployeeId != null (blocks admins with no employee profile from hitting /api/attendance/*)