From 6b8761fb62794d77fe69bf5891cf4f64d10c6dcd Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Wed, 24 Jun 2026 14:21:10 +0200 Subject: [PATCH] plan(s0): chunk 4 - security checklist + rollout + acceptance --- Sprint-0-Plan.md | 204 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/Sprint-0-Plan.md b/Sprint-0-Plan.md index fd3ffc7..3fc7bee 100644 --- a/Sprint-0-Plan.md +++ b/Sprint-0-Plan.md @@ -706,3 +706,207 @@ commit. --- *Plan continues — security review, rollout, acceptance.* + +--- + +## 9. Security review checklist + +The library wraps authentication, so the security review bar is higher than for a typical extraction. +v0.1.0 cannot tag until every item below is verified. + +### 9.1 Secrets + +- [ ] `plate.auth.jwt.secret` and `plate.auth.exchange.secret` are `@NotBlank @Size(min=32)`. App boot + fails if missing or too short. Verified by `PlateAuthPropertiesValidationTest`. +- [ ] No default value for either secret anywhere — not in `application.yml`, not in test resources, + not in `@Value("${...:default}")` fallback. +- [ ] Secrets are read only from env vars / external config. Never logged. JwtService never logs the secret. +- [ ] Test fixtures generate per-test secrets via `UUID.randomUUID()` — no fixed test secrets in repo. +- [ ] `.gitignore` excludes `.env*` (except `.env.example` template). + +### 9.2 HMAC exchange + +- [ ] HMAC algorithm = SHA-256, fixed, not configurable in v0.1. +- [ ] Signature compare uses constant-time comparison + (`MessageDigest.isEqual` on backend, `crypto.subtle.timingSafeEqual` equivalent or `Buffer.compare`+length-check on frontend). +- [ ] Envelope `iat` checked against `now - maxAge`. Default `maxAge=60s`. Configurable. +- [ ] Nonce dedup: every envelope's `nonce` is stored for `nonceTtl` (default 5min). Replay within that + window is rejected with HTTP 401. +- [ ] In-memory nonce store is documented as **single-replica only**. Multi-replica replay protection + deferred to v0.3 (per Roadmap). + +### 9.3 JWT + +- [ ] HMAC SHA-256 signing. +- [ ] Issuer claim is validated against `plate.auth.jwt.issuer` (default `"plate-auth"`). +- [ ] Expiration validated. Tokens without `exp` rejected. +- [ ] No claims contain PII beyond email + user id + role. +- [ ] Refresh-token rotation: a successful refresh issues a new refresh token (and ideally invalidates + the old). v0.1 keeps the InspectFlow-current behavior — tracked for hardening in v0.3. + +### 9.4 SQL + persistence + +- [ ] All repository queries are JPA Criteria or `@Query` with named parameters — no string concat. +- [ ] No `entityManager.createNativeQuery("..." + userInput + "...")` anywhere in moved services. +- [ ] Migrations are SQL files only (no Java-callbacks doing reflection-fueled stuff). +- [ ] Envers `RevInfoListener` populates `actor_user_id` from `SecurityContextHolder` (defensive null check). + +### 9.5 Input validation + +- [ ] All controllers use `@Valid` on `@RequestBody` DTOs. +- [ ] Email fields validated `@Email`. +- [ ] Token fields (invitation, refresh) length-checked to expected size. +- [ ] `OrgType` and other enums use Jackson default behavior (unknown values → 400). +- [ ] No raw `Map` payloads accepted on auth endpoints. + +### 9.6 Error responses + +- [ ] Failed login returns generic "invalid credentials" message — no leak of "user exists" vs + "user doesn't exist." +- [ ] `LoginEvent.outcome=FAILURE` is recorded even on unknown-user attempts. +- [ ] 401 / 403 responses include no stack trace, no SQL fragment, no internal class names. +- [ ] Logging: `log.warn("Login failed for {}", email)` — no password. `log.debug("...")` in JwtService + never logs the secret or full token. + +### 9.7 Audit + +- [ ] Every state-changing op in `MembershipService`, `InvitationService`, `AccessRequestService`, + `AuthService` results in an Envers revision with `actor_user_id` set. +- [ ] `LoginEventService.recordSuccess` and `.recordFailure` cover all four `outcome` enum values. +- [ ] `AdminAuditController` enforces `hasRole('ADMIN')` via method-security annotations. + +### 9.8 OAuth providers + +- [ ] Google `clientId` and `clientSecret` only configured via env, never default. +- [ ] Microsoft Entra ID provider is `@ConditionalOnProperty("plate.auth.providers.microsoft.enabled")`. + Default disabled. If enabled without configured creds → fail-fast at startup. +- [ ] Email magic-link provider is `@ConditionalOnProperty("plate.auth.providers.email-magic-link.enabled")`. + Same fail-fast. +- [ ] `allowDangerousEmailAccountLinking` is **false** in NextAuth config — verified by snapshot test of + `createAuthConfig` output. + +### 9.9 Frontend + +- [ ] `NEXTAUTH_SECRET` documented as required. Library will not start if missing (NextAuth itself + enforces). +- [ ] `NEXTAUTH_EXCHANGE_SECRET` is never sent to the browser. Used server-side only in + `signIn` callback. Validated by reading the bundled output of `@platesoft/auth/config`. +- [ ] Proxy strips hop-by-hop headers (per RFC 7230 + custom `Authorization` override). +- [ ] Proxy never echoes the bearer token in error responses. +- [ ] Edge-runtime compatibility validated by running tests in `@edge-runtime/jest-environment` or + vitest equivalent. + +### 9.10 Dependencies + +- [ ] All deps have CVE scan clean at release tag (Gitea Actions runs Snyk or OWASP dep-check). +- [ ] No transitive dep with known CVE > medium severity. +- [ ] Renovate (or equivalent) configured to keep deps current post-release. + +--- + +## 10. Rollout plan + +### 10.1 Step 0 — internal validation tag + +1. Cut `v0.0.1` from main after all workstreams W1–W7 complete + tests green. +2. In a throwaway repo, consume `de.platesoft:plate-auth-starter:0.0.1` + `@platesoft/auth@0.0.1`. +3. Implement an `OrgValidator` that returns `true` for any input. Boot the app. +4. Hit `/api/auth/config` — should return Google provider info. +5. Sign in via Google. Verify exchange + JWT issued + `/api/auth/me` works. +6. Cleanup: revoke + redeploy. Mark `v0.0.1` as "validated." + +### 10.2 Step 1 — Sparkboard adoption + +Sparkboard is the easier consumer because it has no auth code yet. + +1. Add the Maven dep + npm dep at the chosen `0.1.0` version. +2. Implement Sparkboard's `OrgValidator` against their `workspaces` table. +3. Add env vars: `PLATE_AUTH_JWT_SECRET`, `PLATE_AUTH_EXCHANGE_SECRET`, + `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `NEXTAUTH_SECRET`, `NEXTAUTH_EXCHANGE_SECRET`, + `NEXT_PUBLIC_BACKEND_URL`. +4. Add `app/api/auth/[...nextauth]/route.ts` per [`Integration-Guide.md`](Integration-Guide.md). +5. Boot, sign in, ship. +6. Time-from-zero-to-first-signin = key v0.1 success metric. + +### 10.3 Step 2 — InspectFlow migration + +InspectFlow is harder because it must replace in-tree code without losing data. + +1. Follow [`Migration-InspectFlow.md`](Migration-InspectFlow.md) end-to-end. +2. Critical path: + - Add `plate-auth-starter` dep + - Remove old `de.platesoft.inspectflow.{filter,service,controller}` auth classes + - Rename `inspectflow.*` config props → `plate.auth.*` + - Insert baseline rows into `flyway_schema_history_auth` for V1..V5 (data already exists from V26..V31) + - Add `OrgValidator` impl wrapping `CompanyRepository.existsById(...)` + - Add `OnboardingHook` impl that calls existing `OnboardingService` + - Frontend: replace `lib/auth-config.ts` with a thin wrapper over `createAuthConfig` + - Run full E2E suite — must pass +3. Deploy to staging. +4. Smoke test: sign-in for at least one user of each provider (Google, Microsoft if used, Email if used, + password if registration was enabled). +5. Deploy to production behind feature flag if available; otherwise off-hours deploy with rollback plan. + +### 10.4 Rollback strategy + +If v0.1.0 ships and InspectFlow's adoption breaks something we missed: + +1. **Frontend rollback:** revert the `package.json` dep + re-vendor the old `auth-config.ts`. No DB + change required. +2. **Backend rollback:** revert the `pom.xml` dep, redeploy. Database is unaffected (entities are the + same — just the package names of the classes mapping to them changed). Hibernate `@Table(name="...")` + keeps the SQL schema stable. +3. **Worst case:** restore from pre-deploy database backup + redeploy old commit. Acceptable downtime + window: ≤30min off-hours. + +### 10.5 Acceptance criteria + +v0.1.0 is "done" when: + +| # | Criterion | How verified | +|---|---|---| +| A1 | Maven artifact published at `de.platesoft:plate-auth-starter:0.1.0` | `mvn dependency:get` from a fresh repo succeeds | +| A2 | npm artifact published at `@platesoft/auth@0.1.0` | `npm view @platesoft/auth@0.1.0` from a fresh repo succeeds | +| A3 | InspectFlow runs full E2E suite green using the library | CI green on InspectFlow's migration PR | +| A4 | Sparkboard signs in a user with only Integration-Guide instructions | Stopwatch < 30min from clean repo | +| A5 | All 16 security checklist items in § 9 verified | Security Review document (mode `security-reviewer`) APPROVED | +| A6 | Plan Reviewer APPROVED the plan | Plan-Review document committed to wiki repo | +| A7 | All 10 wiki docs published + cross-references resolve | Visual review by Patrick | +| A8 | CHANGELOG.md released at 0.1.0 | Tag pushed, release notes visible on Gitea | + +--- + +## 11. Items deferred to v0.2+ + +Tracked here so they don't get lost when Sprint 0 closes: + +- Per-tenant JWT issuer config (multi-app sharing a single Postgres) +- Refresh-token rotation table + family-tracking +- `LoginEventSink` SPI for external audit-log shipping +- Multi-replica nonce store via Redis or Postgres `UPSERT ... ON CONFLICT` +- WebAuthn / passkey support +- RFC 7807 Problem Details on error responses +- Configurable invitation expiration (currently hardcoded 7d) +- Default `JavaMailSender`-backed `InvitationMailer` +- Better Edge-runtime test coverage +- TypeScript export of `Membership`, `Invitation`, `AccessRequest` types +- Exported Zod / valibot schemas for envelope and DTOs + +These will form the v0.2 backlog. They are not blockers for v0.1. + +--- + +## 12. Cross-references + +- Assessment: [`Sprint-0-Assessment.md`](Sprint-0-Assessment.md) +- Test plan: [`Sprint-0-Testplan.md`](Sprint-0-Testplan.md) +- Architecture reference: [`Architecture.md`](Architecture.md) +- Open questions blocking final plan: [`Open-Questions.md`](Open-Questions.md) +- Consumer integration: [`Integration-Guide.md`](Integration-Guide.md) +- InspectFlow migration recipe: [`Migration-InspectFlow.md`](Migration-InspectFlow.md) + +--- + +*End of plan.* + +**Status:** Submitted for Plan Reviewer (architect mode) review. Patrick GO required before code starts.