From 861385b4ffb50bc20880a24cb4e62889a2ea28e9 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 19 May 2026 19:46:51 -0400 Subject: [PATCH] docs(idaa): update Novi verification docs to reflect server-side proxy (complete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLIENT__IDAA_and_customized_mods.md: - Verification Flow: describe Aether proxy call, not direct browser-to-Novi fetch - Replace old fetch() code snippet with new Aether endpoint call - Update novi_idaa_api_key / novi_api_root_url field descriptions (server-side only now) - Security notes: key never sent to browser; shape changes go in backend method - Rate limit note: 12h TTL (was 5-min), add 503 auto-retry behavior - Fix Redis cache key: idaa:novi_member:{uuid} (account_id was dropped from key) GUIDE__AE_API_V3_for_Frontend.md §12: - 503 frontend action: auto-retry once after 3s before api_error - Mark migration section complete (2026-05-19); update table to show retry behavior Co-Authored-By: Claude Sonnet 4.6 --- .../CLIENT__IDAA_and_customized_mods.md | 61 ++++++++++--------- .../GUIDE__AE_API_V3_for_Frontend.md | 12 ++-- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/documentation/CLIENT__IDAA_and_customized_mods.md b/documentation/CLIENT__IDAA_and_customized_mods.md index 62234089..c9c57c3c 100644 --- a/documentation/CLIENT__IDAA_and_customized_mods.md +++ b/documentation/CLIENT__IDAA_and_customized_mods.md @@ -113,21 +113,22 @@ IDAA members do not log in through Aether — they log in through Novi (idaa.org > **Security note (2026-03-09):** The iframe HTML files previously also passed `email` and `full_name` > via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed. -> The SvelteKit layout now fetches verified identity directly from the Novi API. +> The SvelteKit layout now verifies identity via the Aether server-side Novi proxy — the Novi API +> call originates from the server, not the member's browser. > See "Iframe Integration" → "Novi UUID Verification Flow" below. ### Verification Flow (`(idaa)/+layout.svelte`) -When a `uuid` param is present in the URL, the layout performs an **async Novi API call** to verify: +When a `uuid` param is present in the URL, the layout performs an **async call to the Aether server-side endpoint** (`GET /v3/action/idaa/novi_member/{uuid}`), which proxies to Novi server-to-server: 1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs) -2. Gets verified name and email directly from Novi — these can't be forged via URL +2. Gets verified name and email — these can't be forged via URL 3. Sets `$idaa_loc.novi_uuid`, `$idaa_loc.novi_email`, `$idaa_loc.novi_full_name` 4. Sets `$idaa_loc.novi_verified = true` on success A `novi_verifying` UI state prevents the "Access Denied" screen from flashing during the API round-trip. -**All or nothing:** If the Novi API key is not configured, or the verification call fails, access is denied. There is no URL-param fallback. +**All or nothing:** If the Novi API key is not configured on the site, or the verification call fails, access is denied. There is no URL-param fallback. **Required `site_cfg_json` fields:** ```json @@ -148,22 +149,26 @@ This section documents the exact way Aether uses the Novi API for the IDAA integ - **All-or-nothing policy:** If the Novi API key is not configured or the verification call fails, the Novi-based access path is denied. The layout explicitly prevents child routes from rendering while verification is in-flight to avoid flashing "Access Denied". -- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The layout handles 429 responses with a 10-second flat backoff and one retry. If the retry also returns 429, access is denied and a "Reload / Retry" button is shown. The 5-minute TTL cache on successful verification prevents repeated calls during normal use. +- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The Aether backend handles 429 responses; the frontend receives a `429` and retries once after 10 seconds. The 12-hour TTL cache on successful verification (Redis server-side + `$idaa_loc` client-side) prevents repeated calls during normal use. A `503` (Novi unreachable) is auto-retried once after 3 seconds before surfacing an error to the user. ### Verification Flow (implementation) 1. The IDAA iframe loads Aether pages with a `?uuid=&iframe=true` param. -2. When the `uuid` param is present the IDAA layout performs an authenticated GET against the Novi customers endpoint: +2. When the `uuid` param is present the IDAA layout calls the Aether server-side proxy: ```js // simplified -fetch(`${api_root_url}/customers/${uuid}`, { +fetch(`${aether_api_url}/v3/action/idaa/novi_member/${uuid}`, { method: 'GET', - headers: { 'Authorization': `Basic ${api_key}` } + headers: { + 'x-aether-api-key': api_key, + 'x-account-id': account_id + } }) +// Aether calls Novi server-to-server; member's browser IP is never in the Novi call path. ``` -3. On success the layout uses the returned JSON to build a display name and normalized email, then writes these values to the IDAA store and marks verification success. +3. On success (`200`), the layout reads `data.full_name` and `data.email` from the response and writes them to the IDAA store, marking verification success. 4. The layout then determines a target Novi permission level (`authenticated`, `trusted`, `administrator`) by checking configured UUID lists (`novi_trusted_li`, `novi_admin_li`) and upgrades the Aether session only if the Novi-derived level is higher than the current global level. @@ -171,9 +176,9 @@ fetch(`${api_root_url}/customers/${uuid}`, { ### Key `site_cfg_json` fields and where they are used -- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Required for the verification request. Accessed in code as `$ae_loc.site_cfg_json.novi_idaa_api_key` and passed in the `Authorization: Basic ` header. If missing, Novi-based access is denied. +- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Used by the Aether **server** to authenticate against Novi — the frontend never touches the key itself. The frontend checks only for its *presence* in `site_cfg_json` as a guard meaning "IDAA is configured for this site". If missing, Novi-based access is denied. -- **`novi_api_root_url`**: Optional API root (defaults to `https://www.idaa.org/api`). Used to form the verification URL. +- **`novi_api_root_url`**: Optional Novi API root (defaults to `https://www.idaa.org/api`). Read by the Aether server, not the frontend. - **`novi_admin_li`**: Array of UUIDs treated as administrators for IDAA. Merged into `$idaa_loc.novi_admin_li` during layout initialization and used to set `administrator` level. @@ -214,35 +219,31 @@ These fields are read elsewhere in the IDAA UI to enable flows for verified user ### Security notes and operational guidance - The previous implementation leaked `email` and `full_name` via URL params — this was removed because those values are unauthenticated and can be spoofed. -- The API key is sensitive — keep it only in site config and do not expose it in client-side code or public repositories. -- The verification request uses Basic auth with the provided `novi_idaa_api_key` (already Base64-encoded by Novi) — treat the token like a password. -- If Novi changes their customer API shape, update the layout parsing (display name/email normalization) and this documentation. +- The API key is sensitive — keep it only in site `cfg_json` and do not expose it in client-side code or public repositories. The key is read and used exclusively by the Aether backend; it is never sent to the browser. +- If Novi changes their customer API shape, update `app/methods/idaa_novi_verify_methods.py` in the backend (display name/email normalization) and this documentation. If you need a compact checklist for re-creating this flow in another integration, ask and I will add a small runbook with exact request/response field mappings. -### Planned: Server-Side Novi Verification (FastAPI) +### ~~Planned: Server-Side Novi Verification~~ ✅ Implemented (2026-05-19) -**Problem:** The current implementation calls the Novi API client-side — from the member's browser directly to Novi. Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering can block these calls and produce false "Access Denied" for legitimate members. +**Problem solved:** The previous client-side Novi API call originated from the member's browser. +Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering +could block these calls and produce false "Access Denied" for legitimate members. -**Solution:** A FastAPI endpoint proxies the Novi call server-to-server (Aether → Novi), caching results in Redis. Members' browser IPs are no longer in the call path. +**Solution implemented:** A FastAPI endpoint proxies the Novi call server-to-server +(Aether → Novi), with Redis caching. Members' browser IPs are no longer in the call path. **Endpoint:** `GET /v3/action/idaa/novi_member/{uuid}` -- Standard Aether auth headers required (`x-aether-api-key`, `x-account-id`) +- Standard Aether auth headers (`x-aether-api-key`, `x-account-id`) - Server reads `novi_idaa_api_key` / `novi_api_root_url` from site `cfg_json` -- Redis cache key: `idaa:novi_member:{account_id}:{uuid}` — TTL 4 hours, only cache verified 200s +- Redis cache: `idaa:novi_member:{uuid}` — 4-hour TTL, only 200s cached +- `404` results never cached (recently-joined members not incorrectly denied) -**Response codes:** +**Frontend:** `verify_novi_uuid()` in `(idaa)/+layout.svelte` now calls this endpoint with +standard Aether headers. The `novi_idaa_api_key` is still checked for presence in +`site_cfg_json` as a proxy for "is IDAA configured for this site" (server holds the key itself). -| Code | Meaning | Frontend action | -|---|---|---| -| `200` | Verified — `{ "verified": true, "full_name": "...", "email": "..." }` | Grant access | -| `404` | UUID not in Novi (genuine non-member) | Deny access | -| `429` | Novi rate limited | Show retry UI (not a denial) | -| `503` | Novi unreachable | Show retry UI (not a denial) | - -**Frontend change when implemented:** Replace the direct `fetch()` to Novi in `verify_novi_uuid()` with a call to this endpoint via `ae_api`. The `api_key` param becomes unused (server holds it). Response code mapping: 404 → denied, 429 → `'rate_limited'`, 503 → `'api_error'`. - -**FastAPI task:** Tracked in `aether_api_fastapi/documentation/TODO__Agents.md` under "IDAA: Server-Side Novi Verification". +**Full API spec:** `GUIDE__AE_API_V3_for_Frontend.md` §12. ### Permission Levels (Ascending) | Level | Condition | Access | diff --git a/documentation/GUIDE__AE_API_V3_for_Frontend.md b/documentation/GUIDE__AE_API_V3_for_Frontend.md index d36bf0d5..fece6e29 100644 --- a/documentation/GUIDE__AE_API_V3_for_Frontend.md +++ b/documentation/GUIDE__AE_API_V3_for_Frontend.md @@ -673,19 +673,19 @@ Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether |---|---|---| | `404` | UUID not found in Novi, or Novi returned 200 with no identity data (empty-member anti-pattern — member may have just joined) | Treat as denied / not a member | | `429` | Novi rate limit hit | Surface as `'rate_limited'`; advise retry | -| `503` | Novi unreachable or Novi 5xx error | Surface as `'api_error'`; advise retry | +| `503` | Novi unreachable or Novi 5xx error | Auto-retry once after 3s; if retry also fails, surface as `'api_error'` | -### Migration from direct Novi call +### Migration from direct Novi call — ✅ Complete (2026-05-19) -The frontend's `+layout.svelte:verify_novi_uuid()` currently calls Novi directly from the browser. Replace that `fetch()` with this endpoint. Response code mapping: +`+layout.svelte:verify_novi_uuid()` now calls this endpoint instead of Novi directly. Response code mapping (for reference): -| Direct Novi result | This endpoint returns | Frontend state | +| Direct Novi result | This endpoint returns | Frontend behavior | |---|---|---| | `200` with identity data | `200` | `verified` | | `200` with no identity data | `404` | `denied` | | `404` | `404` | `denied` | -| `429` | `429` | `'rate_limited'` | -| Network error / Novi 5xx | `503` | `'api_error'` | +| `429` | `429` | Auto-retry after 10s; `'rate_limited'` if retry fails | +| Network error / Novi 5xx | `503` | Auto-retry after 3s; `'api_error'` if retry fails | ### Caching