From 2f137afd5f0642b8a9226a4097a579b8a1d928b3 Mon Sep 17 00:00:00 2001 From: Skylar Grant Date: Thu, 15 Oct 2026 21:29:25 -0400 Subject: [PATCH] Limit sign-in to allowed email domains Any Zoho account can approve the client, and an outside account got a session that returned the .env site, group and technician names. ALLOWED_EMAIL_DOMAINS is now required: the callback refuses other domains and revokes their token, and every API call re-checks it so older cookies stop working. /api/names also makes the caller's token read from SDP before returning the shared names. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ docs/NOTES.md | 2 ++ docs/ROADMAP.md | 1 + src/env.example | 3 +++ src/names.js | 7 +++++-- src/public/index.html | 1 + src/routes.js | 18 +++++++++++++++++- src/server.js | 15 ++++++++++++--- src/session.js | 7 +++++-- src/tickets.js | 6 ++++++ 10 files changed, 54 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6acfcaf..167499a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Copy `src/env.example` to a file named `.env` and fill it in. The region, timing | --- | --- | --- | | `CLIENT_ID`, `CLIENT_SECRET` | Yes | The Zoho Server-based Application credentials. | | `SESSION_SECRET` | Yes | At least 32 characters. Encrypts the sign-in cookie. Generate one with `openssl rand -hex 32`. Changing it signs everyone out. | +| `ALLOWED_EMAIL_DOMAINS` | Yes | Comma-separated email domains allowed to sign in, e.g. `mainecc.edu`. Any Zoho user can approve the client, so this is what keeps outside Zoho accounts out. Exact match: `mainecc.edu` doesn't let in `sub.mainecc.edu`. Checked at sign-in and on every call, so removing a domain signs those people out. The server won't start without it. | | `PUBLIC_URL` | Yes when hosted | The address people open, with no trailing slash, e.g. `https://dashboard.example.com`. Zoho sends people back to `PUBLIC_URL/auth/callback`. Defaults to `http://localhost:3000`. Cookies are marked secure when it starts with `https://`. | | `SESSION_DAYS` | No | How many days a sign-in lasts before Zoho is asked again. Defaults to 7. | | `TECHNICIAN_NAMES` | No | Comma-separated technician names, spelled as they appear in SDP. One table row per name. Leave it empty to include every technician. | @@ -108,6 +109,7 @@ The sign-in is kept in an encrypted cookie in that browser for `SESSION_DAYS` (7 ### Security - **Network:** NPM only lets in MCCS' public IP addresses (campuses and VPN). From anywhere else the subdomain doesn't answer. +- **Allowed domains:** any Zoho account can complete the Zoho consent screen, so the server only creates a session for emails at `ALLOWED_EMAIL_DOMAINS`, and checks again on every call. A refused sign-in has its Zoho token revoked straight away. - **Per-user sign-in:** every request needs the user's own Zoho sign-in. There is no shared token, so a reachable page with no sign-in shows nothing. - **Tokens:** the Zoho tokens live only in the user's cookie, encrypted and authenticated with `SESSION_SECRET` (AES-256-GCM). The cookie is `HttpOnly`, so page scripts can't read it, and `SameSite=Lax`. The server doesn't store tokens or sessions. - **Read-only:** the only SDP scope is `SDPOnDemand.requests.READ`. diff --git a/docs/NOTES.md b/docs/NOTES.md index 3994d65..80615ac 100644 --- a/docs/NOTES.md +++ b/docs/NOTES.md @@ -40,6 +40,8 @@ Docs: