docs(idaa): update Novi verification docs to reflect server-side proxy (complete)

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 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-19 19:46:51 -04:00
parent 42f40e990e
commit 861385b4ff
2 changed files with 37 additions and 36 deletions

View File

@@ -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=<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 <key>` 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 |

View File

@@ -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