plan(s0): chunk 4 - security checklist + rollout + acceptance

Patrick Plate
2026-06-24 14:21:10 +02:00
parent 75758699b2
commit 6b8761fb62
+204
@@ -706,3 +706,207 @@ commit.
--- ---
*Plan continues — security review, rollout, acceptance.* *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<String,Object>` 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 W1W7 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.