---
type: Architecture Overview
title: Authentication & authorization
description: Google ID-token verification, the session cookie, and the admin/employee role split.
tags: [architecture, auth, security]
timestamp: 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](/docs/data-model/employees.md).

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
- `requireAdmin` — `role === 'admin'`
- `requireEmployee` — `employeeId != null` (blocks admins with no employee
  profile from hitting `/api/attendance/*`)

## Related

- [System overview](./overview.md)
- [Auth API](/docs/api/auth-routes.md)
- [Employees table](/docs/data-model/employees.md)
