mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-06 00:42:48 +02:00
docs(next-gen): reconcile auth config with multi-tenancy; rename OIDC_SESSION_SECRET to JWT_SESSION_SECRET
This commit is contained in:
@@ -212,7 +212,7 @@ Browser ──EventSource('/api/v1/events/stream')──→ Web tier
|
||||
|
||||
The web tier already proxies all `/api/v1/*` calls — SSE is just a long-lived, streaming variant of the same proxy pattern. The implementation requirement: **the proxy must pipe chunks as they arrive, not buffer the response.** In Fastify, this means writing to `reply.raw` (the underlying `ServerResponse`) and calling `reply.raw.flushHeaders()` before the first chunk, or using `@fastify/http-proxy` which handles streaming correctly out of the box.
|
||||
|
||||
**Single-process alternative:** if the deployment runs web + API as one Fastify process (the simpler model for small/community deployments), no proxy is needed. The `AuthMiddleware` resolves the Principal directly from the cookie (same `OIDC_SESSION_SECRET`, same JWS verification the web tier uses). This adds a fourth resolution path to the middleware (after JWT-header, API-key, anonymous): cookie → verify JWS → resolve Principal. The auth boundary is unchanged — the middleware is still the single resolution point; the Principal is still the single authz artifact. The JWT becomes optional in this mode (the cookie is the credential, verified inline rather than transmitted over a network).
|
||||
**Single-process alternative:** if the deployment runs web + API as one Fastify process (the simpler model for small/community deployments), no proxy is needed. The `AuthMiddleware` resolves the Principal directly from the cookie (same `JWT_SESSION_SECRET`, same JWS verification the web tier uses). This adds a fourth resolution path to the middleware (after JWT-header, API-key, anonymous): cookie → verify JWS → resolve Principal. The auth boundary is unchanged — the middleware is still the single resolution point; the Principal is still the single authz artifact. The JWT becomes optional in this mode (the cookie is the credential, verified inline rather than transmitted over a network).
|
||||
|
||||
### SSE realtime endpoint (concrete)
|
||||
|
||||
@@ -268,7 +268,7 @@ Not all config is the same. Split by *when it can change*:
|
||||
|
||||
```
|
||||
DATABASE_URL, NATS_URL, MQTT_HOST/PORT/..., REDIS_HOST/..., OIDC_CLIENT_ID/SECRET/DISCOVERY,
|
||||
OIDC_SESSION_SECRET, API_HOST/PORT, LOG_LEVEL, INSTANCE_NAME
|
||||
JWT_SESSION_SECRET, API_HOST/PORT, LOG_LEVEL, INSTANCE_NAME
|
||||
```
|
||||
|
||||
Rule of thumb: if changing it requires reconnecting to an external system or re-authenticating, it's Tier 1. Secrets stay here (or in a secret manager) — they shouldn't sit in a DB backup.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Auth
|
||||
|
||||
> **Related decisions:** D6 (auth boundary — JWT issued by the web tier, verified at API middleware; `X-User-*` header injection removed), D12 (multi-source auth — OIDC optional, built-in local password store always available, `AUTH_MODE=local|oidc|hybrid` default **hybrid**, all sources converge on one JWT issuance), D18 (CLI for ops, not config — one config surface per item).
|
||||
> **Related decisions:** D6 (auth boundary — JWT issued by the web tier, verified at API middleware; `X-User-*` header injection removed), D12 (multi-source auth — OIDC optional, built-in local password store always available, `AUTH_MODE=local|oidc|hybrid` default **hybrid**, all sources converge on one JWT issuance; per-tenant `auth_mode`/IdP in multi-tenant mode — multi-tenancy.md §6), D18 (CLI for ops, not config — one config surface per item).
|
||||
>
|
||||
> **Note:** Code examples are illustrative pseudocode showing design patterns (shapes, flows, contracts).
|
||||
> The implementation uses the TypeScript stack (D22): Fastify middleware, jose (JWT/JWS), argon2, Zod.
|
||||
@@ -24,7 +24,7 @@ flowchart LR
|
||||
|
||||
The defining property: **the API is credential-source-agnostic.** It verifies a JWT and resolves a `Principal`; it never knows *how* the web tier authenticated the user. That abstraction is what lets us offer multiple login methods without the API caring:
|
||||
|
||||
- **Web tier** mints a short-lived JWT (signed `HS256`/`RS256` with `OIDC_SESSION_SECRET`) carrying `sub`, `roles`, `instance_id`, `exp`, stored in the existing signed cookie. Every login path converges here.
|
||||
- **Web tier** mints a short-lived JWT (signed `HS256`/`RS256` with `JWT_SESSION_SECRET`) carrying `sub`, `roles`, `instance_id`, `exp`, stored in the existing signed cookie. Every login path converges here.
|
||||
- **API** verifies the JWT at a single middleware; handlers receive a resolved `Principal` (with `role_tier`, `user_id`, `instance_id`) via `Depends`. No more `X-User-*` header injection — the JWT *is* the credential. Fixes S1, S2.
|
||||
- **Direct Bearer (API keys)** remain for m2m/CLI; they map to a `Principal` with a fixed role.
|
||||
- **Channel visibility** (redaction, below) is computed from the `Principal`, once, per request.
|
||||
@@ -39,7 +39,7 @@ Three credential sources, each optional, all producing the same JWT:
|
||||
| **OIDC/OAuth2** *(optional — configure if you have an IdP)* | Org/multi-user deployments wanting SSO | Existing redirect/callback flow; roles from the IdP claim |
|
||||
| **API keys** *(always — for automation)* | CLI, scripts, other services talking to the API directly | Bearer token verified at the API middleware (no web tier involved) |
|
||||
|
||||
`AUTH_MODE` (Tier-1 env var) selects which interactive sources the login page offers:
|
||||
`AUTH_MODE` selects which interactive sources the login page offers. It is a Tier-1 env var in single-tenant mode; in multi-tenant mode it is per-tenant (`tenant_oidc_configs.auth_mode`) with the env var as the platform default (multi-tenancy.md §6):
|
||||
|
||||
- `local` — username/password form only. Zero external dependencies.
|
||||
- `oidc` — "Sign in with SSO" button only. For orgs that mandate the IdP.
|
||||
@@ -91,13 +91,13 @@ local password verified ┘ → sets meshcore-session cookie
|
||||
|
||||
- **Local users** are managed via a Users admin page (fits naturally in the Settings UI / D11). Admins create users, assign roles, reset passwords, disable accounts — all runtime, no env-var changes.
|
||||
- **OIDC users** are still provisioned just-in-time on first login (as today), with roles from the IdP claim; an admin can promote/demote via the same Users page.
|
||||
- The `AUTH_MODE` setting itself stays Tier-1 (env var) because it affects which bootstrap credentials are required.
|
||||
- The `AUTH_MODE` env var is the platform default (Tier-1, because it gates bootstrap). In multi-tenant mode each tenant overrides it via `tenant_oidc_configs.auth_mode` — a runtime DB setting requiring no restart (multi-tenancy.md §6).
|
||||
|
||||
## JWT token shape & session model
|
||||
|
||||
Two artifacts: a **session cookie** (long-lived, the "refresh") and an **access JWT** (short-lived, per-request credential). The web tier mints a fresh access JWT from the session on each proxied request; the API only ever sees non-expired access JWTs. No separate refresh token reaches the API.
|
||||
|
||||
**Access JWT claims (HS256, signed with `OIDC_SESSION_SECRET`):**
|
||||
**Access JWT claims (HS256, signed with `JWT_SESSION_SECRET`):**
|
||||
```json
|
||||
{
|
||||
"iss": "meshcore-hub",
|
||||
@@ -113,7 +113,7 @@ Two artifacts: a **session cookie** (long-lived, the "refresh") and an **access
|
||||
```
|
||||
|
||||
- **5-minute access lifetime** — short enough that a stolen token has a tiny window, long enough that the web tier's per-request re-mint doesn't fight itself under burst load.
|
||||
- **7-day session cookie** (`meshcore-session`, HttpOnly, SameSite=Lax, signed with `OIDC_SESSION_SECRET` via `jose` JWS — same mechanism, updated library). Sliding renewal: each proxied request reissues if past half-life.
|
||||
- **7-day session cookie** (`meshcore-session`, HttpOnly, SameSite=Lax, signed with `JWT_SESSION_SECRET` via `jose` JWS — same mechanism, updated library). Sliding renewal: each proxied request reissues if past half-life.
|
||||
- **RS256 option:** if an operator wants asymmetric signing (web tier holds private key, API verifies with public key), expose via `JWT_SIGNING_ALG=rs256` + `JWT_PRIVATE_KEY`/`JWT_PUBLIC_KEY` PEM paths. Default HS256 — single issuer, simpler ops.
|
||||
|
||||
## The Principal (resolved once per request)
|
||||
@@ -165,7 +165,7 @@ async function resolve(request: FastifyRequest): Promise<Principal> {
|
||||
}
|
||||
// 2. Session cookie (browser access — single-process mode, or SSE via proxy)
|
||||
// Only active when this process also serves the web tier. The cookie is
|
||||
// a JWS signed with OIDC_SESSION_SECRET; verify inline → resolve Principal.
|
||||
// a JWS signed with JWT_SESSION_SECRET; verify inline → resolve Principal.
|
||||
const cookie = request.cookies?.["meshcore-session"];
|
||||
if (cookie && this.cookieVerifier) {
|
||||
const session = this.cookieVerifier.verify(cookie); // throws on tamper/expiry → 401
|
||||
@@ -218,7 +218,7 @@ fastify.addHook("preHandler", async (request: FastifyRequest, reply: FastifyRepl
|
||||
|
||||
1. **Welcome / network identity** — network name, city, country, contact (writes the Tier-2 branding settings that are otherwise empty on a fresh DB).
|
||||
2. **Admin account** — username + password + confirm. Creates the first admin (see bootstrap insert sequence below).
|
||||
3. **Auth mode** — local (default) / oidc (enter IdP config) / hybrid. Sets the Tier-1 `AUTH_MODE`-equivalent as a setting.
|
||||
3. **Auth mode** — local (default) / oidc (enter IdP config) / hybrid. Written as a per-instance setting (`tenant_oidc_configs.auth_mode` in multi-tenant mode, seeded from the `AUTH_MODE` platform default).
|
||||
4. **Feature flags** — which pages to enable (sensible defaults pre-checked).
|
||||
5. **Done** — sets `fastify.state.needsSetup = false`, redirects to the dashboard, logged in as the new admin.
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ NATS_URL=nats://localhost:4222
|
||||
REDIS_URL=redis://localhost:6379
|
||||
MQTT_HOST=localhost
|
||||
MQTT_PORT=1883
|
||||
OIDC_SESSION_SECRET=dev-secret-not-for-production
|
||||
JWT_SESSION_SECRET=dev-secret-not-for-production
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
@@ -421,11 +421,11 @@ Needed to start the process; can't be read from the DB. ~20 vars. Everything els
|
||||
| `MQTT_PASSWORD` | No | — | ingester |
|
||||
| `MQTT_TLS` | No | `false` | ingester |
|
||||
| `REDIS_URL` | No | — (disabled) | api |
|
||||
| `OIDC_SESSION_SECRET` | Yes | — | web, api (JWT signing) |
|
||||
| `OIDC_CLIENT_ID` | No | — | web (OIDC, if configured) |
|
||||
| `OIDC_CLIENT_SECRET` | No | — | web |
|
||||
| `OIDC_DISCOVERY_URL` | No | — | web |
|
||||
| `AUTH_MODE` | No | `hybrid` | web (`local`/`oidc`/`hybrid`) |
|
||||
| `JWT_SESSION_SECRET` | Yes | — | web, api — **platform** JWT + session-cookie signing key, shared across all tenants (the trust anchor; cannot be per-tenant) |
|
||||
| `OIDC_CLIENT_ID` | No | — | web — platform-default IdP (fallback when a tenant has no own config in `tenant_oidc_configs`) |
|
||||
| `OIDC_CLIENT_SECRET` | No | — | web — platform-default IdP secret |
|
||||
| `OIDC_DISCOVERY_URL` | No | — | web — platform-default IdP discovery URL |
|
||||
| `AUTH_MODE` | No | `hybrid` | web — platform-default `local`/`oidc`/`hybrid`; per-tenant override in `tenant_oidc_configs.auth_mode` |
|
||||
| `API_HOST` | No | `0.0.0.0` | api |
|
||||
| `API_PORT` | No | `3000` | api |
|
||||
| `WEB_HOST` | No | `0.0.0.0` | web |
|
||||
@@ -442,6 +442,8 @@ Needed to start the process; can't be read from the DB. ~20 vars. Everything els
|
||||
| `BLOB_STORE_ENDPOINT` | No | — | worker (`BLOB_STORE_TYPE=s3`) |
|
||||
| `BLOB_STORE_BUCKET` | No | — | worker |
|
||||
|
||||
**Platform-scope vs tenant-scope auth config.** `JWT_SESSION_SECRET` is genuinely platform-wide: one key signs the JWT/session cookie for *every* tenant, and the API trusts the `instance_id` claim because the platform signed it (it cannot be per-tenant — the API learns the tenant *from* the JWT this key signs). The `OIDC_CLIENT_*` and `AUTH_MODE` vars are **platform defaults only**: in multi-tenant mode (D21, Phase 7) each tenant overrides them via `tenant_oidc_configs` (their own IdP + `auth_mode`), resolved per hostname; the env vars are the fallback when a tenant hasn't configured their own (multi-tenancy.md §6).
|
||||
|
||||
### CLI commands
|
||||
|
||||
Run via `docker compose run --rm <service>` in production, or `npx tsx src/cli.ts` in development.
|
||||
|
||||
@@ -22,7 +22,7 @@ flowchart TB
|
||||
ING[MqttIngester<br/>decodes ALL traffic<br/>routes per tenant]
|
||||
end
|
||||
MQTT --> ING
|
||||
ING -->|observer → tenant lookup| ROUTE{ObserverAllowlistCache<br/>dict[prefix → set[tenant_id]]}
|
||||
ING -->|observer → tenant lookup| ROUTE{"ObserverAllowlistCache<br/>dict[prefix → set[tenant_id]]"}
|
||||
|
||||
ROUTE -->|"O1 → Tenant A"| NS_A[(NATS: meshcore.ingest.<A>.*)]
|
||||
ROUTE -->|"O2 → Tenant A+B"| NS_A & NS_B
|
||||
@@ -407,7 +407,7 @@ async function instanceResolution(request: FastifyRequest, reply: FastifyReply)
|
||||
|
||||
- `GET /api/v1/config` returns the **tenant's** settings (branding, features, pages, auth_mode).
|
||||
- The login page renders the **tenant's** auth options (local form, OIDC button, or both per the tenant's `auth_mode`).
|
||||
- JWT issuance uses the **tenant's** OIDC config (for the OIDC redirect) and the platform's `OIDC_SESSION_SECRET` (for signing — shared across tenants, since the API verifies with the same key).
|
||||
- JWT issuance uses the **tenant's** OIDC config (for the OIDC redirect) and the platform's `JWT_SESSION_SECRET` (for signing — shared across tenants, since the API verifies with the same key).
|
||||
- The setup wizard (D12) runs per-tenant: a fresh tenant with no admin sees the wizard on their hostname.
|
||||
- The platform's root domain (`meshhub.example.com`) serves a **landing page** with a "Create your community" link → the registration flow (§8).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Today's auth is two overlapping planes with an implicit trust boundary (S1): dir
|
||||
|
||||
**Short-lived JWT issued by the web tier, verified at a single API middleware.**
|
||||
|
||||
- **Web tier** mints an access JWT (default HS256 signed with `OIDC_SESSION_SECRET`; optional RS256 via `JWT_SIGNING_ALG=rs256` + `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` PEM paths) carrying `sub`, `roles`, `role_tier` (pre-resolved), `instance_id`, `type=access`, `iat`, `exp` (+5 minutes), `jti`. Stored in the existing signed `meshcore-session` cookie (7-day sliding renewal via `jose` JWS — same mechanism, updated library). Every login path (local password, OIDC callback — see D12) converges here.
|
||||
- **Web tier** mints an access JWT (default HS256 signed with `JWT_SESSION_SECRET`; optional RS256 via `JWT_SIGNING_ALG=rs256` + `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` PEM paths) carrying `sub`, `roles`, `role_tier` (pre-resolved), `instance_id`, `type=access`, `iat`, `exp` (+5 minutes), `jti`. Stored in the existing signed `meshcore-session` cookie (7-day sliding renewal via `jose` JWS — same mechanism, updated library). Every login path (local password, OIDC callback — see D12) converges here.
|
||||
- **API** verifies the JWT at a single `AuthMiddleware`; handlers receive a frozen `Principal` (`user_id`, `roles`, `role_tier`, `instance_id`, `channel_indices` pre-resolved) via `Depends`. **No more `X-User-*` header injection** — the JWT *is* the credential.
|
||||
- **Direct Bearer (API keys)** remain for CLI/automation; they map to a `Principal` with a fixed role at the same middleware.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Today ~200 env vars configure everything from branding to tuning to feature flag
|
||||
|
||||
**Three-tier config model.** A setting is in exactly one tier, determined by *when it can change*:
|
||||
|
||||
- **Tier 1 — Bootstrap (env vars, immutable at runtime, ~15–20 vars).** Needed to start the process or reach the DB/IdP: `DATABASE_URL`, `NATS_URL`, `MQTT_HOST/...`, `REDIS_HOST/...`, `OIDC_CLIENT_ID/SECRET/DISCOVERY`, `OIDC_SESSION_SECRET`, `API_HOST/PORT`, `LOG_LEVEL`, `INSTANCE_NAME`. Rule of thumb: if changing it requires reconnecting to an external system or re-authenticating, it is Tier 1. Secrets stay here (or in a secret manager) — never in a DB backup.
|
||||
- **Tier 1 — Bootstrap (env vars, immutable at runtime, ~15–20 vars).** Needed to start the process or reach the DB/IdP: `DATABASE_URL`, `NATS_URL`, `MQTT_HOST/...`, `REDIS_HOST/...`, `OIDC_CLIENT_ID/SECRET/DISCOVERY`, `JWT_SESSION_SECRET`, `API_HOST/PORT`, `LOG_LEVEL`, `INSTANCE_NAME`. Rule of thumb: if changing it requires reconnecting to an external system or re-authenticating, it is Tier 1. Secrets stay here (or in a secret manager) — never in a DB backup. In multi-tenant mode (D21) the OIDC/`AUTH_MODE` vars are **platform defaults only** — each tenant overrides them via `tenant_oidc_configs` (a DB entity, multi-tenancy.md §6); `JWT_SESSION_SECRET` stays platform-wide because it signs for all tenants.
|
||||
- **Tier 2 — Runtime settings (DB-backed `settings` table, UI-editable, cached + NATS-invalidated).** Branding/content, feature flags, tuning (retention, spam thresholds, evaluator intervals, cache TTLs), webhooks, radio display. Exposed via `GET/PUT /api/v1/settings/{category}`; per-category Zod validation on write; defaults ship as a seed migration; cross-service invalidation via NATS `settings.updated.<inst>.{category}`. Env vars override the seed at first boot only, then the DB is authoritative.
|
||||
- **Tier 3 — First-class entities (already DB-backed, unchanged).** Channels, routes, tags, profiles. Custom pages move to DB in D20 (previously file-based).
|
||||
|
||||
|
||||
@@ -17,19 +17,21 @@ Today interactive login is OIDC-only (with an API-key plane for m2m). That requi
|
||||
| **OIDC/OAuth2** | Org/multi-user deployments wanting SSO | Optional — configure if you have an IdP |
|
||||
| **API keys** (direct Bearer) | CLI, scripts, m2m | Yes — for automation |
|
||||
|
||||
`AUTH_MODE` (Tier-1 env var, because it affects which bootstrap credentials are required) selects which interactive sources the login page offers:
|
||||
`AUTH_MODE` selects which interactive sources the login page offers. It is a Tier-1 env var in single-tenant mode (because it affects which bootstrap credentials are required at first boot); in multi-tenant mode (D21, Phase 7) it becomes a **per-tenant** setting (`tenant_oidc_configs.auth_mode`) with the env var as the platform default:
|
||||
|
||||
- `local` — username/password form only. Zero external dependencies.
|
||||
- `oidc` — "Sign in with SSO" button only. For orgs that mandate the IdP.
|
||||
- **`hybrid` (default)** — both: the login page shows the OIDC button *and* the username/password form. Operators pick per user.
|
||||
|
||||
**Multi-tenant evolution (D21).** A single global `AUTH_MODE`/IdP would force every self-provisioned tenant into the platform operator's auth choice — contradicting self-service. So in Phase 7 the IdP config *and* `auth_mode` move to the per-tenant `tenant_oidc_configs` table (multi-tenancy.md §6): a community tenant runs local passwords, an enterprise tenant points at their own Okta, both on one platform. The Tier-1 `OIDC_CLIENT_*`/`AUTH_MODE` vars survive only as **platform-level defaults** (the fallback when a tenant hasn't configured their own). The one thing that stays irreducibly platform-wide is the JWT/session signing key (`JWT_SESSION_SECRET`, renamed from `OIDC_SESSION_SECRET` — it was never OIDC config): one key signs for all tenants, and the API trusts the `instance_id` claim because the platform signed it.
|
||||
|
||||
A `local_users` row links to a `user_profiles` row (adoptions, tags, route ownership work unchanged — keyed on `user_profile.id`). The profile's `user_id` is namespaced `local:<username>` to avoid colliding with OIDC `sub` claims. Password hashing: argon2id (`argon2` npm package, native bindings), parameterised at install time. Rate limiting: `failed_attempts` + `locked_until` give exponential lockout (5 → 15s, 10 → 5m, 20 → 1h) without Redis. Bootstrap admin via `ADMIN_USERNAME`/`ADMIN_PASSWORD` env, `admin create-user` CLI, or the first-run setup wizard (§20.8). Local + OIDC users share one Users management page.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:** Zero-dependency default deployment — the smallest operator runs the stack and logs in without an IdP. The D6 JWT boundary means adding/removing credential sources later doesn't touch the API. Argon2id + exponential lockout is the modern password-store baseline. One Users admin page manages both sources.
|
||||
|
||||
**Negative:** Two code paths to maintain (local verify + OIDC callback) — though both converge on JWT issuance. Local password storage is a security responsibility (hashing, lockout, rotation) that OIDC-only avoided. The `AUTH_MODE` setting is Tier-1 (env var) because it gates bootstrap, so changing it requires a restart.
|
||||
**Negative:** Two code paths to maintain (local verify + OIDC callback) — though both converge on JWT issuance. Local password storage is a security responsibility (hashing, lockout, rotation) that OIDC-only avoided. The `AUTH_MODE` env var is Tier-1 in single-tenant mode because it gates bootstrap, so changing the platform default requires a restart; the per-tenant `auth_mode` (Phase 7) is a runtime DB setting and needs no restart.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -38,4 +40,4 @@ A `local_users` row links to a `user_profiles` row (adoptions, tags, route owner
|
||||
| **Multi-source, `hybrid` default** (chosen) | OIDC optional; zero-dependency default; one JWT boundary. |
|
||||
| OIDC-only (today's model) | Rejected — forces every deployment to run an IdP; blocks the community-operator use case. |
|
||||
| Local-only (drop OIDC) | Rejected — loses SSO for org/multi-user deployments; regresses an existing capability. |
|
||||
| Per-instance auth mode in DB (not env) | Rejected — auth mode gates which bootstrap credentials are required, so it must be available before the DB is reachable. |
|
||||
| Per-instance auth mode in DB (not env) | Rejected as the *single-tenant bootstrap* mechanism (auth mode gates which bootstrap credentials are required, so it must predate DB reachability) — but **adopted for multi-tenant runtime** in Phase 7 (D21): `tenant_oidc_configs.auth_mode`, with the env var as platform default. |
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
### Auth
|
||||
- [ ] Implement `AuthMiddleware` preHandler (JWT → cookie → API key → anonymous) — [auth.md](components/auth.md#authmiddleware-single-resolution-point)
|
||||
- [ ] Implement `Principal` frozen object + resolution from JWT claims / session cookie / API key
|
||||
- [ ] Implement JWT issuance in the web tier (5m access, HS256, `OIDC_SESSION_SECRET`)
|
||||
- [ ] Implement JWT issuance in the web tier (5m access, HS256, `JWT_SESSION_SECRET`)
|
||||
- [ ] Implement session-cookie sliding renewal (7d, JWS via `jose`)
|
||||
- [ ] Implement local password store: `local_users` table, argon2id verify, exponential lockout
|
||||
- [ ] Implement the shared 3-table bootstrap insert (user_profiles + local_users + user_profile_roles) in one transaction
|
||||
|
||||
@@ -75,7 +75,7 @@ The cleanup phase after all functional phases land. Three tracks:
|
||||
### 6.2 Security hardening pass
|
||||
- [ ] RLS audit: verify every tenant-scoped table enforces the `instance_id` policy; add a test that asserts cross-instance queries return 0 rows.
|
||||
- [ ] Rate-limit review: confirm local-login lockout thresholds; verify reverse-proxy `limit_req` / `fail2ban` rules for the auth endpoint.
|
||||
- [ ] JWT rotation drill: rotate `OIDC_SESSION_SECRET`; verify all sessions invalidate gracefully.
|
||||
- [ ] JWT rotation drill: rotate `JWT_SESSION_SECRET`; verify all sessions invalidate gracefully.
|
||||
- [ ] Dependency audit: `npm audit` clean (backend + frontend); no known CVEs in the lockfiles.
|
||||
|
||||
### 6.3 New-repo `AGENTS.md` / `CONTRIBUTING.md`
|
||||
|
||||
Reference in New Issue
Block a user