Table of Contents
- Sprint 0 — Implementation Plan
- Reading guide
- 1. Scope + ground rules
- 2. Workstream overview
- Phase 1 — Extraction scaffold (partially complete)
- Phase 2 — Completion to v0.1.0 (defined in §13 below)
- 3. Backend extraction — overview
- 4. Frontend extraction — overview
- 5. Flyway strategy — overview
- 6. Build + publish pipeline — overview
- 4. Backend extraction — step-by-step
- 4.1 W1 — Repo scaffolding
- 4.2 W2-A — Backend: copy + rename classes
- 4.3 W2-B — Configuration namespace
- 4.4 W2-C — Auto-configuration
- 4.5 W4 — SPI design + default implementations
- 6. Frontend extraction — step-by-step
- 6.1 W3-A — npm package skeleton
- 6.2 W3-B — Move + factor frontend code
- 6.3 W3-C — Boilerplate Next.js route file
- 7. Flyway migration consolidation
- 8. Build + publish pipeline
- 9. Security review checklist
- 9.1 Secrets
- 9.2 HMAC exchange
- 9.3 JWT
- 9.4 SQL + persistence
- 9.5 Input validation
- 9.6 Error responses
- 9.7 Audit
- 9.8 OAuth providers
- 9.9 Frontend
- 9.10 Dependencies
- 10. Rollout plan
- 10.1 Step 0 — internal validation tag
- 10.2 Step 1 — Sparkboard adoption
- 10.3 Step 2 — InspectFlow migration
- 10.4 Rollback strategy
- 10.5 Acceptance criteria
- 11. Items deferred to v0.2+
- 13. Phase 2 — Completion workstreams (W8–W12)
- 13.1 W8 — Backend extraction completion
- 13.2 W9 — Frontend implementation
- 13.3 W10 — N1 fix + unit tests
- 13.4 W11 — Security + integration + Envers
- 13.5 W12 — Polish + validation
- 13.6 Phase 2 — completion summary
- 14. Cross-references
Sprint 0 — Implementation Plan
Status: Draft v2 (Phase 2 — completion path endorsed by Review v3 @ 79%)
Date: 2026-06-24
Owner: Patrick (plate-software)
Based on: Sprint-0-Assessment.md, Architecture.md, Sprint-0-Plan-Review-v3.md
Target version: 0.1.0 (both Maven + npm)
Decision: ✅ Path A — full v0.1.0 extraction (Q15 resolved). Phase 1 (W1–W7) partially complete; Phase 2 (W8–W12) defined below to close the remaining 13 gaps.
Reading guide
This is the single longest document in the wiki. It is structured as:
- Scope + ground rules (this section)
- Workstream overview (the 7 parallel-ish workstreams)
- Backend extraction — step-by-step
- Frontend extraction — step-by-step
- Flyway migration consolidation
- Build + publishing pipeline
- Security review checklist (must pass before v0.1.0 tag)
- Rollout plan (InspectFlow + Sparkboard adoption)
- Acceptance criteria
- Open items deferred to v0.2+
Each step is numbered so the Code-mode worker can check off progress without ambiguity.
1. Scope + ground rules
1.1 In scope (Sprint 0)
- New git repo
plate-auth(already created —git.plate-software.de/pplate/plate-auth) - Backend artifact
de.platesoft:plate-auth-starter:0.1.0(Maven, Spring Boot 4 starter) - Frontend artifact
@platesoft/auth@0.1.0(npm, NextAuth v5 wiring) - 5 SPIs (
OrgValidator,OrgDisplayNameResolver,InvitationMailer,AccessRequestMailer,OnboardingHook) - Flyway migrations under
db/migration/auth/V1..V6(6 migrations — see § 5 andArchitecture.md§ 8.1 for the canonical list) - Gitea Actions pipeline that publishes both artifacts on a
v*git tag - Internal integration tests covering exchange + JWT + memberships
- All 10 wiki docs reviewed + approved by Plan Reviewer + GO from Patrick
1.2 Out of scope (deferred — see Roadmap.md)
- WebAuthn / passkeys (v0.2+)
- Multi-replica nonce store (v0.3)
- Refresh-token rotation table (v0.3)
- JWT secret rotation via
kid(v0.3) - External audit-log sink (v0.3)
LoginEventSinkSPI (v0.3)- SAML, SCIM, OIDC server, mobile SDKs (post-1.0, demand-driven)
1.3 Ground rules
- No new features. This sprint is an extraction. Anything beyond what InspectFlow already does in Sprint 14.1–14.6 is automatic v0.2 fodder.
- Single git repo, two artifacts. Maven module + npm package live in one repo with a shared
CHANGELOG.mdand lockstep versions. - Tests must pass on day 1. No "TODO: write tests later" lines in committed code.
- Plan Reviewer is mandatory before code. No code starts until the plan is APPROVED.
- Branch policy:
feature/sprint-0/<workstream>branches → PR → squash-merge tomain.
2. Workstream overview
Phase 1 (W1–W7): Extraction scaffold — partially complete. The repo scaffolding, OAuth exchange core, entities, Flyway V1–V6, 5 SPIs + defaults, and review-v2-fixes (B1/B2/B4/B5/W-A/W-C) are done. Branch:
feature/sprint-0/review-v2-fixes@b43ab5e.Phase 2 (W8–W12): Completion path to v0.1.0 — defined in §13. Closes the 13 remaining gaps identified by Review v3 (score 79% → target 96–97%).
flowchart LR
subgraph Phase1[Phase 1 — W1..W7: scaffold + core]
W1[W1: Repo scaffolding] --> W2[W2: Backend extraction]
W1 --> W3[W3: Frontend extraction]
W2 --> W4[W4: SPI design]
W2 --> W5[W5: Flyway consolidation]
W3 --> W6[W6: Build pipeline]
W4 --> W6
W5 --> W6
W6 --> W7[W7: Integration tests]
end
subgraph Phase2[Phase 2 — W8..W12: completion to v0.1.0]
W7 --> W8[W8: Backend completion<br/>controllers + services]
W3 --> W9[W9: Frontend implementation<br/>createAuthConfig + proxy]
W8 --> W10[W10: N1 fix + unit tests]
W9 --> W10
W10 --> W11[W11: Security + IT + Envers]
W11 --> W12[W12: Polish + validation tag]
end
Phase 1 — Extraction scaffold (partially complete)
| # | Workstream | Status | Owner | Depends on |
|---|---|---|---|---|
| W1 | Repo scaffolding (Maven + npm structure, CI skeleton, README) | ✅ Done | Code mode | — |
| W2 | Backend extraction — class moves, package renames, config namespace | 🟡 Partial (exchange/OAuth core only — B3) | Code mode | W1 |
| W3 | Frontend extraction — createAuthConfig factory, proxy factory, types |
🔴 Stubs (throws NotYetImplemented) | Code mode | W1 |
| W4 | SPI design — 5 interfaces + default no-op implementations + @ConditionalOnMissingBean wiring |
✅ Done | Code mode | W2 |
| W5 | Flyway migration consolidation + flyway_schema_history_auth decision |
✅ Done (V1–V6) | Code mode | W2 |
| W6 | Gitea Actions publish pipeline — Maven + npm to Gitea Package Registry | 🟡 CI skeleton only (no packages published) | Code mode | W2, W3, W4, W5 |
| W7 | Integration tests + InspectFlow dry-run migration | 🟡 3 of 9 ITs (bootstrap, exchange, Flyway) | Code mode | All |
Phase 2 — Completion to v0.1.0 (defined in §13 below)
| # | Workstream | Owner | Depends on |
|---|---|---|---|
| W8 | Backend extraction completion — AuthController, Invitation/AccessRequest/AdminAudit + OrgContextResolver | Code mode | W2 |
| W9 | Frontend implementation — createAuthConfig, signEnvelope, createProxyHandlers, middleware, hooks | Code mode | W3 |
| W10 | N1 fix + unit tests — rename deprecated web starter, implement T-UT01..15 | Code mode | W8 |
| W11 | Security + integration + Envers — T-SEC01..10, T-IT04..09, RevInfo entity | Code mode | W10 |
| W12 | Polish + validation — Flyway co-existence test, CHANGELOG/README, resolve Open Qs, v0.0.1 tag | Code mode | W11 |
| Phase 2 total | ~6–7 engineering days · projected score 96–97% |
3. Backend extraction — overview
See § 4 for step-by-step.
The backend extraction is fundamentally:
- Move every class in
inspectflow/backend/src/main/java/de/platesoft/inspectflow/{config,filter,service,controller,entity,repository,dto,enums}/...that is auth-related intoplate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/... - Rename every import,
@Value("${jwt....}")→@Value("${plate.auth.jwt....}")etc. - Replace every direct
companiesrepository call insideMembershipServiceetc. with a call throughOrgValidator/OrgDisplayNameResolverSPI. - Add
PlateAuthAutoConfiguration+PlateAuthProperties+META-INF/spring/...AutoConfiguration.imports. - Add default no-op SPI implementations annotated
@ConditionalOnMissingBean.
No new domain logic. No new endpoints. No new entity fields.
4. Frontend extraction — overview
See § 6 for step-by-step.
The frontend extraction is:
- Move the NextAuth config from
inspectflow/frontend/lib/auth-config.tsinto acreateAuthConfig(opts)factory inplate-auth/packages/auth/src/config/index.ts - Move the proxy logic from
inspectflow/frontend/app/api/[...path]/route.tsinto acreateProxyHandlers(opts)factory - Move the HMAC exchange logic from
inspectflow/frontend/lib/exchange.tsintoplate-auth/packages/auth/src/exchange/ - Replace the InspectFlow-specific
localStoragekeys with configurable prefixes - Export typed hooks (
useAccessToken,useMemberships) and re-export NextAuth'suseSession,signIn,signOut
5. Flyway strategy — overview
See § 7 for step-by-step.
Decision (to be ratified by Patrick — see Open-Questions.md Q03):
- plate-auth ships migrations under
classpath:db/migration/auth/V1..V6(6 migrations — V1–V6; seeArchitecture.md§ 8.1) - Consumers configure Flyway with multiple locations + separate history table for plate-auth:
spring.flyway.locations=classpath:db/migration,classpath:db/migration/auth # Default app history table stays "flyway_schema_history" # plate-auth's history is tracked in the SAME table but with V1..V6 from auth/ prefixed via # an installed_rank shift OR — preferred — a *second* Flyway managed by plate-auth's auto-config. - For InspectFlow, V26–V31 already ran. We do not re-run them. Migration recipe:
- Insert a "baseline" row into
flyway_schema_history_authmarking V1..V6 as already applied - Detailed steps in
Migration-InspectFlow.md
- Insert a "baseline" row into
Final pattern is locked in § 7. The Plan Reviewer must approve the chosen approach before code starts.
6. Build + publish pipeline — overview
See § 8 for step-by-step.
- Single repo.
mvn -pl plate-auth-starter packagebuilds the JAR.npm -w @platesoft/auth buildbuilds the npm package. - A git tag
v0.1.0triggers a Gitea Actions workflow that:- Builds + tests both artifacts
- Publishes the JAR to Gitea Package Registry (Maven)
- Publishes the npm tarball to Gitea Package Registry (npm)
- Updates the
CHANGELOG.mdwith the release date
- Snapshot builds (on every push to
main) publish to0.1.0-SNAPSHOT/0.1.0-snapshot.N.
Plan continues in the next section — backend extraction step-by-step.
4. Backend extraction — step-by-step
4.1 W1 — Repo scaffolding
Goal: Create the directory layout + skeleton pom.xml files + npm workspace structure.
Steps:
- W1-1 In the
plate-authrepo root, create:plate-auth/ ├── pom.xml # parent POM, packaging=pom ├── plate-auth-starter/ │ ├── pom.xml # the published artifact │ └── src/{main,test}/{java,resources}/ ├── packages/ │ └── auth/ │ ├── package.json # @platesoft/auth │ └── src/ ├── package.json # workspace root ├── pnpm-workspace.yaml # if pnpm; npm/yarn equivalent works ├── .gitea/workflows/ # Gitea Actions ├── CHANGELOG.md └── README.md - W1-2 Parent
pom.xml:<groupId>de.platesoft</groupId>,<artifactId>plate-auth-parent</artifactId>,<version>${revision}</version>(CI-injected),<packaging>pom</packaging>,<modules><module>plate-auth-starter</module></modules>. Inherit fromspring-boot-starter-parent:4.1.0. - W1-3
plate-auth-starter/pom.xml:<artifactId>plate-auth-starter</artifactId>, deps copied from InspectFlowbackend/pom.xmlminus app-specific (no ONNX, no openpdf, no pdfbox):web, data-jpa, security, validation, actuator, mail, hibernate-envers, postgresql, flyway, jjwt-api/impl/jackson, mapstruct, lombok, logstash-logback, h2 (test scope), testcontainers (test scope) - W1-4 Root
package.json+pnpm-workspace.yamlconfigured to pick uppackages/*. - W1-5
packages/auth/package.json:"name": "@platesoft/auth","version": "0.1.0", peerDeps onnext@>=15,next-auth@^5.0.0-beta,react@>=19. - W1-6 Add
.gitignore,.editorconfig, basicREADME.mdlinking to the wiki. - W1-7 Commit + push to
main. CI must pass on empty modules (just compiles).
Done when: mvn -B verify and pnpm -r build both succeed on a fresh clone.
4.2 W2-A — Backend: copy + rename classes
Goal: Mechanical move from de.platesoft.inspectflow.* → de.platesoft.auth.* for every auth class.
Steps (per source class — repeat for the inventory in Sprint-0-Assessment § 1.1):
- W2-1 Copy class file from
inspectflow/backend/src/main/java/de/platesoft/inspectflow/<pkg>/<X>.javatoplate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/<pkg>/<X>.java. - W2-2 Update
packagedeclaration:package de.platesoft.auth.<pkg>;. - W2-3 Update internal imports (i.e. imports of other classes we are also moving) to the new package. Use search-replace, but verify each file compiles independently after.
- W2-4 Leave behind: any reference to
Company,OnboardingService,TenantAutoMapService— replace with an SPI call (see W4 below). DO NOT move these classes.
Class-by-class checklist:
filter/JwtAuthenticationFilter.java— direct move, no rewritesfilter/OrgContextResolver.java— direct moveconfig/SecurityConfig.java— direct move, but: replace InspectFlow-specific permit-list (/api/companies/*/public-info) with config-driven list fromPlateAuthProperties.cors.additionalPermitPathsservice/JwtService.java— direct move; rename@Value("${jwt.secret}")→@Value("${plate.auth.jwt.secret}")service/OAuthService.java— move + swaptenantAutoMapService.maybeAutoMap(...)call foronboardingHook.onFirstSignIn(...)(SPI)service/ExchangeService.java— direct moveservice/MembershipService.java— move + swapcompanyRepository.findById(...)fororgValidator.exists(orgType, orgId)/orgDisplayNameResolver.displayName(...)callsservice/InvitationService.java— direct move (Sprint 14.3 already abstracted the mailer)service/AccessRequestService.java— direct moveservice/LoginEventService.java— direct moveservice/AuthService.java— direct move (password login/register)controller/AuthController.java— direct movecontroller/OAuthController.java— direct movecontroller/InvitationController.java— direct movecontroller/AccessRequestController.java— direct movecontroller/AdminAuditController.java— direct moveentity/User.java— direct moveentity/UserIdentity.java— direct moveentity/Membership.java— direct moveentity/Invitation.java— direct moveentity/AccessRequest.java— direct moveentity/LoginEvent.java— direct moveentity/RevInfo.java(Envers revinfo with actor) — direct moverepository/*— all auth-related repositories — direct movedto/request/*anddto/response/*— direct moveenums/*— direct move (Role, OrgType, MembershipRole, MembershipStatus, InvitationStatus, AccessRequestStatus, LoginProvider)
Done when: plate-auth-starter compiles in isolation. No references to de.platesoft.inspectflow.*
remain in moved classes.
4.3 W2-B — Configuration namespace
Goal: Consolidate all @Value("${...}") injections into a single
@ConfigurationProperties("plate.auth") class.
Steps:
- W2-8 Create
de.platesoft.auth.PlateAuthProperties:@ConfigurationProperties(prefix = "plate.auth") @Data public class PlateAuthProperties { private Jwt jwt = new Jwt(); private Exchange exchange = new Exchange(); private Registration registration = new Registration(); private Cors cors = new Cors(); private Providers providers = new Providers(); @Data public static class Jwt { private String secret; private Duration accessExpiration = Duration.ofMinutes(15); private Duration refreshExpiration = Duration.ofDays(30); private String issuer = "plate-auth"; } @Data public static class Exchange { private String secret; private Duration maxAge = Duration.ofSeconds(60); private Duration nonceTtl = Duration.ofMinutes(5); } @Data public static class Registration { private boolean enabled = false; } @Data public static class Cors { private List<String> allowedOrigins = new ArrayList<>(); private List<String> additionalPermitPaths = new ArrayList<>(); } @Data public static class Providers { private ProviderToggle google = new ProviderToggle(true); private ProviderToggle microsoft = new ProviderToggle(false); private ProviderToggle emailMagicLink = new ProviderToggle(false); } @Data @AllArgsConstructor @NoArgsConstructor public static class ProviderToggle { private boolean enabled; } } - W2-9 Inject
PlateAuthProperties(constructor injection) into every service that previously read@Value("${jwt....}")etc. Replace@Valueannotations. - W2-10 Add
META-INF/spring-configuration-metadata.json(or rely onspring-boot-configuration-processorannotation processor — preferred — add it to the build). - W2-11 Bean-validate critical fields:
@NotBlank @Size(min=32) private String secret; // both jwt.secret and exchange.secret
Done when: No @Value("${jwt....}") or @Value("${nextauth....}") strings remain. All config flows
through PlateAuthProperties. App fails fast at startup if secret is missing or <32 chars.
4.4 W2-C — Auto-configuration
Goal: Make the starter "just work" when added as a dependency.
Steps:
- W2-12 Create
de.platesoft.auth.PlateAuthAutoConfiguration:@AutoConfiguration @EnableConfigurationProperties(PlateAuthProperties.class) @ComponentScan(basePackages = "de.platesoft.auth") @EntityScan(basePackages = "de.platesoft.auth.entity") @EnableJpaRepositories(basePackages = "de.platesoft.auth.repository") @ConditionalOnProperty(prefix = "plate.auth", name = "enabled", havingValue = "true", matchIfMissing = true) public class PlateAuthAutoConfiguration { // Default SPI beans, ConditionalOnMissingBean — see W4 } - W2-13 Register the auto-config:
Contents: single line
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsde.platesoft.auth.PlateAuthAutoConfiguration. - W2-14 Verify that adding the starter to a minimal Spring Boot 4 app, setting
plate.auth.jwt.secretplate.auth.exchange.secretenv vars, andspring.datasource.urlto a Postgres, makes the app boot and serve/api/auth/config.
Done when: Smoke test app (in plate-auth-tests/) boots and responds to /api/auth/config.
4.5 W4 — SPI design + default implementations
Goal: Five SPI interfaces with sensible defaults so consumers can override selectively.
Steps:
- W4-1 Create the 5 SPI interfaces under
de.platesoft.auth.spi:public interface OrgValidator { boolean exists(OrgType type, UUID orgId); } public interface OrgDisplayNameResolver { String displayName(OrgType type, UUID orgId); } public interface InvitationMailer { void sendInvitation(Invitation invitation, String acceptUrl); } public interface AccessRequestMailer { void notifyAdmins(AccessRequest request); void notifyRequester(AccessRequest request); // on decision } public interface OnboardingHook { void onFirstSignIn(User user, LoginProvider provider); default void onSubsequentSignIn(User user, LoginProvider provider) { /* no-op */ } } - W4-2 Default implementations (annotated
@ConditionalOnMissingBean, registered inPlateAuthAutoConfiguration):PermissiveOrgValidator— ships as the default.exists(...)always returnstrueand logs a WARN on every call:"OrgValidator default permissive — override de.platesoft.auth.spi.OrgValidator bean before production". Rationale: the starter must boot green with zero consumer code; the per-call WARN makes it impossible to ship to production without noticing that real validation is missing.DefaultOrgDisplayNameResolver— returnstype + ":" + orgId.toString()LoggingInvitationMailer— logs the accept URL at INFO levelLoggingAccessRequestMailer— logs notifications at INFO levelNoOpOnboardingHook— no-op
- W4-3 Wire each service to its SPI dep via constructor injection:
MembershipService←OrgValidator,OrgDisplayNameResolverInvitationService←InvitationMailer,OrgDisplayNameResolverAccessRequestService←AccessRequestMailer,OrgDisplayNameResolverOAuthService←OnboardingHook
- W4-4 Document each SPI with Javadoc including:
- When it is called
- What happens if the consumer doesn't provide one (which default kicks in)
- Migration: if a previous version's signature changed, link to CHANGELOG
Done when: Starter boots green with zero SPI beans (default PermissiveOrgValidator accepts
all (org_type, org_id) and emits per-call WARN). A consumer can register a single OrgValidator
bean to replace it and have T2 fully validated. Default mailers log instead of crashing.
Plan continues — frontend extraction, Flyway, publishing.
6. Frontend extraction — step-by-step
6.1 W3-A — npm package skeleton
Goal: A buildable, publishable @platesoft/auth@0.1.0 with TypeScript + ESM/CJS dual build.
Steps:
- W3-1 Configure
packages/auth/package.json:{ "name": "@platesoft/auth", "version": "0.1.0", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" }, "./config": { "import": "./dist/config/index.js", "types": "./dist/config/index.d.ts" }, "./exchange": { "import": "./dist/exchange/index.js", "types": "./dist/exchange/index.d.ts" }, "./proxy": { "import": "./dist/proxy/index.js", "types": "./dist/proxy/index.d.ts" }, "./middleware":{ "import": "./dist/middleware/index.js","types": "./dist/middleware/index.d.ts" }, "./client": { "import": "./dist/client/index.js", "types": "./dist/client/index.d.ts" } }, "peerDependencies": { "next": ">=15.0.0", "next-auth": "^5.0.0-beta", "react": ">=19.0.0" }, "files": ["dist", "README.md", "LICENSE"] } - W3-2 Bundler choice:
tsup(zero-config dual ESM/CJS, fast). Addtsup.config.tstargeting Node 20 + Edge runtime. - W3-3 TypeScript strict config,
"target": "ES2022","module": "ESNext","moduleResolution": "Bundler","declaration": true. - W3-4 Add a
publishConfigblock pointing to the Gitea npm registry (set in W6).
Done when: pnpm -F @platesoft/auth build produces dist/ with ESM + CJS + .d.ts files.
6.2 W3-B — Move + factor frontend code
Steps (per file from inspectflow/frontend/):
- W3-5 Copy
frontend/lib/exchange.ts→packages/auth/src/exchange/client.ts.- Replace
import { ... } from "@/lib/..."patterns with relative imports inside the package. - Extract the envelope-signing logic into
packages/auth/src/exchange/envelope.ts:export interface ExchangeEnvelope { provider: 'google' | 'microsoft' | 'email' | 'password'; providerSubject: string; email: string; name?: string; inviteToken?: string; nonce: string; iat: number; // unix seconds } export function signEnvelope(env: ExchangeEnvelope, secret: string): { envelope: string; signature: string }; export function makeNonce(): string; // crypto.randomUUID() - Use Web Crypto API (
crypto.subtle.importKey+sign("HMAC", ...)) so the code runs in the Edge runtime as well as Node.
- Replace
- W3-6 Copy
frontend/lib/auth-config.ts→packages/auth/src/config/index.ts.- Refactor into a factory:
export interface PlateAuthConfigOptions { providers: { google?: GoogleOpts; microsoft?: MicrosoftOpts; email?: EmailOpts }; exchange: { backendUrl: string; secret: string; appLabel?: string }; session?: { strategy?: 'jwt'; maxAge?: number }; callbacks?: { afterSignIn?: (user: PlateAuthUser) => Promise<void> }; trustHost?: boolean; } export function createAuthConfig(opts: PlateAuthConfigOptions): NextAuthConfig { // builds provider list from opts.providers // signIn callback calls exchangeWithBackend(envelope) using opts.exchange // jwt callback persists access_token + memberships from backend response // session callback exposes accessToken to client } - Provider modules under
packages/auth/src/config/providers/{google,microsoft,email}.tsfor clean tree-shaking.
- Refactor into a factory:
- W3-7 Copy
frontend/app/api/[...path]/route.ts→packages/auth/src/proxy/handlers.ts.- Refactor:
export interface ProxyOptions { backendUrl: string; stripHeaders?: string[]; // default: hop-by-hop list authHeaderName?: string; // default: 'Authorization' } export function createProxyHandlers(opts: ProxyOptions): { GET: RouteHandler; POST: RouteHandler; PUT: RouteHandler; PATCH: RouteHandler; DELETE: RouteHandler; OPTIONS: RouteHandler; }; - Must use NextAuth v5
auth()notgetToken(). Body forwarding must includeduplex: "half"for streaming POST/PUT.
- Refactor:
- W3-8 Copy
frontend/middleware.ts→packages/auth/src/middleware/index.ts.- Factor as
createAuthMiddleware(opts?: { publicPaths?: string[] })returning aNextMiddleware.
- Factor as
- W3-9 Move
frontend/contexts/auth-context.tsxlogic intopackages/auth/src/client/hooks.ts— but as hooks only, no React Context wrapper. Consumers build their own provider if needed.- Expose:
export function useAccessToken(): string | null; export function useMemberships(): Membership[]; export type { Membership, OrgType, MembershipRole, MembershipStatus };
- Expose:
- W3-10 Re-export NextAuth client surface in
packages/auth/src/client/index.ts:export { useSession, signIn, signOut, SessionProvider } from "next-auth/react"; export * from "./hooks";
Done when: Library compiles, exports listed above resolve, and a sample Next.js app can
import { createAuthConfig } from '@platesoft/auth/config'.
6.3 W3-C — Boilerplate Next.js route file
Consumers need a one-line route file. We ship documentation, not the file itself
(it must live in their app/api/auth/[...nextauth]/route.ts):
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { createAuthConfig } from '@platesoft/auth/config';
const config = createAuthConfig({
providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! } },
exchange: { backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL!, secret: process.env.NEXTAUTH_EXCHANGE_SECRET! },
});
export const { handlers, auth, signIn, signOut } = NextAuth(config);
export const { GET, POST } = handlers;
Documented in Integration-Guide.md.
7. Flyway migration consolidation
7.1 Strategy
After Plan Reviewer feedback on Open-Questions Q03, finalize the strategy. The recommended approach for v0.1 (subject to Plan Reviewer concurrence):
Separate Flyway history table for plate-auth migrations.
- Consumer config:
spring.flyway.locations=classpath:db/migration,classpath:db/migration/auth- plate-auth auto-configures a second
Flywaybean namedplateAuthFlywaywith:
locations = classpath:db/migration/authtable = flyway_schema_history_auth- Runs at startup before the application's own Flyway
- Application's primary
Flywaycontinues to manageflyway_schema_historyfor app migrationsWhy: plate-auth's
V1..V6numbering is completely independent of any app'sV1..VN. Both libraries can advance their own version space without collision. Consumers get a clean install from scratch, and InspectFlow'sMigration-InspectFlow.mdhandles the in-place baseline.
If Plan Reviewer rejects this and prefers numbered-tail approach (e.g. plate-auth ships V1..V6 and relies on app migrations starting at V100), we revise to single-table strategy. Both approaches are viable; the separate-table one is more isolating.
7.2 W5 — Migration files
Steps:
- W5-1 Create
plate-auth-starter/src/main/resources/db/migration/auth/directory. - W5-2 Copy V26 →
V1__create_users_and_identities.sql. Edit:- Remove anything InspectFlow-specific (none expected)
- Verify Postgres compatibility (no H2-only syntax)
- W5-3 Copy V27 →
V2__create_memberships.sql. Drop the triggerfn_membership_org_fk()from the migration — that trigger referencescompanieswhich is T3. Consumers add their own trigger or rely solely on theOrgValidatorSPI for validation.- Document in
Migration-InspectFlow.md: "InspectFlow's V27 trigger was migrated; if you previously relied on it, keep it in your app's migration."
- Document in
- W5-4 Copy V28 →
V3__create_invitations.sql. - W5-5 Copy V29 →
V4__create_access_requests.sql. - W5-6 Create
V5__add_microsoft_tenant_id_index.sql. This is a standalone migration that adds an index onuser_identities.microsoft_tenant_id(the column itself lives in V1). It stays separate from the login-events migration so the index can ship as a hotfix without renumbering. - W5-7 Copy V31 →
V6__create_login_events_and_revinfo_actor.sql. (V30,companies.microsoft_tenant_id, stays in InspectFlow's migration set — T3.) - W5-8 Add
MigrationContentTest(integration test) that:- Spins up Testcontainers Postgres
- Runs plate-auth Flyway against
flyway_schema_history_auth - Asserts all 6 versions applied successfully (
V1..V6) - Asserts no SQL errors in clean install
Done when: Migration test passes against Testcontainers Postgres in CI with 6 rows in
flyway_schema_history_auth.
7.3 W5 — Auto-config the second Flyway bean
@Configuration
@ConditionalOnClass(Flyway.class)
public class PlateAuthFlywayConfig {
@Bean
public Flyway plateAuthFlyway(DataSource dataSource) {
Flyway fw = Flyway.configure()
.dataSource(dataSource)
.locations("classpath:db/migration/auth")
.table("flyway_schema_history_auth")
.baselineOnMigrate(true) // for fresh installs only
.load();
fw.migrate();
return fw;
}
}
Critical detail: this Bean's migrate() must run before any @Entity is touched. Spring Boot's
default Flyway runs as part of JPA initialization; we run ours explicitly in the bean factory method.
Integration tests verify ordering.
8. Build + publish pipeline
8.1 W6-A — Gitea Actions workflow
Steps:
- W6-1 Create
.gitea/workflows/ci.yml:name: CI on: push: { branches: [main] } pull_request: { branches: [main] } jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '25', distribution: 'temurin' } - uses: actions/setup-node@v4 with: { node-version: '22' } - run: npm install -g pnpm - run: mvn -B verify - run: pnpm install --frozen-lockfile - run: pnpm -r build - run: pnpm -r test - W6-2 Create
.gitea/workflows/release.yml:name: Release on: push: { tags: ['v*'] } jobs: publish-maven: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { java-version: '25', distribution: 'temurin' } - name: Configure Maven for Gitea run: | mkdir -p ~/.m2 cat > ~/.m2/settings.xml <<EOF <settings> <servers><server> <id>gitea</id> <username>${{ secrets.GITEA_USER }}</username> <password>${{ secrets.GITEA_TOKEN }}</password> </server></servers> </settings> EOF - run: mvn -B -Drevision=${GITHUB_REF_NAME#v} deploy publish-npm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' registry-url: 'https://git.plate-software.de/api/packages/pplate/npm/' - run: npm install -g pnpm - run: pnpm install --frozen-lockfile - name: Set version from tag run: pnpm -F @platesoft/auth version ${GITHUB_REF_NAME#v} --no-git-tag-version - run: pnpm -F @platesoft/auth build - run: pnpm -F @platesoft/auth publish --no-git-checks env: NPM_CONFIG_TOKEN: ${{ secrets.GITEA_TOKEN }} - W6-3 Add
distributionManagementblock to parentpom.xmlpointing at the Gitea Maven endpoint (https://git.plate-software.de/api/packages/pplate/maven). - W6-4 Snapshot publishing on every push to
main:- Maven:
mvn -Drevision=0.1.0-SNAPSHOT deploy(Gitea Package Registry allows SNAPSHOT-style for Maven) - npm: skip on snapshots, or use
pnpm publish --tag snapshotwith0.1.0-snapshot.<sha>version
- Maven:
Done when: Pushing tag v0.0.1 publishes both de.platesoft:plate-auth-starter:0.0.1 (Maven) and
@platesoft/auth@0.0.1 (npm) to the Gitea Package Registry. Verified by mvn dependency:get + npm view.
8.2 W6-B — Validation tag
Before cutting v0.1.0, cut v0.0.1 first:
- Verifies the publish pipeline end-to-end
- Lets InspectFlow team try
mvn dependency:get de.platesoft:plate-auth-starter:0.0.1 - Forces us to fix all the inevitable "wrong settings.xml / missing token" issues before the real release
After v0.0.1 lands cleanly and is consumed in a throwaway test app, cut v0.1.0 from the same
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.secretandplate.auth.exchange.secretare@NotBlank @Size(min=32). App boot fails if missing or too short. Verified byPlateAuthPropertiesValidationTest.- 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. .gitignoreexcludes.env*(except.env.exampletemplate).
9.2 HMAC exchange
- HMAC algorithm = SHA-256, fixed, not configurable in v0.1.
- Signature compare uses constant-time comparison
(
MessageDigest.isEqualon backend,crypto.subtle.timingSafeEqualequivalent orBuffer.compare+length-check on frontend). - Envelope
iatchecked againstnow - maxAge. DefaultmaxAge=60s. Configurable. - Nonce dedup: every envelope's
nonceis stored fornonceTtl(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
exprejected. - 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
@Querywith 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
RevInfoListenerpopulatesactor_user_idfromSecurityContextHolder(defensive null check).
9.5 Input validation
- All controllers use
@Validon@RequestBodyDTOs. - Email fields validated
@Email. - Token fields (invitation, refresh) length-checked to expected size.
OrgTypeand 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=FAILUREis 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,AuthServiceresults in an Envers revision withactor_user_idset. LoginEventService.recordSuccessand.recordFailurecover all fouroutcomeenum values.AdminAuditControllerenforceshasRole('ADMIN')via method-security annotations.
9.8 OAuth providers
- Google
clientIdandclientSecretonly 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. allowDangerousEmailAccountLinkingis false in NextAuth config — verified by snapshot test ofcreateAuthConfigoutput.
9.9 Frontend
NEXTAUTH_SECRETdocumented as required. Library will not start if missing (NextAuth itself enforces).NEXTAUTH_EXCHANGE_SECRETis never sent to the browser. Used server-side only insignIncallback. Validated by reading the bundled output of@platesoft/auth/config.- Proxy strips hop-by-hop headers (per RFC 7230 + custom
Authorizationoverride). - Proxy never echoes the bearer token in error responses.
- Edge-runtime compatibility validated by running tests in
@edge-runtime/jest-environmentor 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
- Cut
v0.0.1from main after all workstreams W1–W7 complete + tests green (6 Flyway migrations applied, defaultPermissiveOrgValidatoremits WARN on every call). - In a throwaway repo, consume
de.platesoft:plate-auth-starter:0.0.1+@platesoft/auth@0.0.1. - Implement an
OrgValidatorthat returnstruefor any input. Boot the app. - Hit
/api/auth/config— should return Google provider info. - Sign in via Google. Verify exchange + JWT issued +
/api/auth/meworks. - Cleanup: revoke + redeploy. Mark
v0.0.1as "validated."
10.2 Step 1 — Sparkboard adoption
Sparkboard is the easier consumer because it has no auth code yet.
- Add the Maven dep + npm dep at the chosen
0.1.0version. - Implement Sparkboard's
OrgValidatoragainst theirworkspacestable. - 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. - Add
app/api/auth/[...nextauth]/route.tsperIntegration-Guide.md. - Boot, sign in, ship.
- 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.
- Follow
Migration-InspectFlow.mdend-to-end. - Critical path:
- Add
plate-auth-starterdep - Remove old
de.platesoft.inspectflow.{filter,service,controller}auth classes - Rename
inspectflow.*config props →plate.auth.* - Insert baseline rows into
flyway_schema_history_authfor V1..V6 (data already exists from V26..V31) - Add
OrgValidatorimpl wrappingCompanyRepository.existsById(...) - Add
OnboardingHookimpl that calls existingOnboardingService - Frontend: replace
lib/auth-config.tswith a thin wrapper overcreateAuthConfig - Run full E2E suite — must pass
- Add
- Deploy to staging.
- Smoke test: sign-in for at least one user of each provider (Google, Microsoft if used, Email if used, password if registration was enabled).
- 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:
- Frontend rollback: revert the
package.jsondep + re-vendor the oldauth-config.ts. No DB change required. - Backend rollback: revert the
pom.xmldep, 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. - 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
LoginEventSinkSPI 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-backedInvitationMailer - Better Edge-runtime test coverage
- TypeScript export of
Membership,Invitation,AccessRequesttypes - Exported Zod / valibot schemas for envelope and DTOs
These will form the v0.2 backlog. They are not blockers for v0.1.
13. Phase 2 — Completion workstreams (W8–W12)
Context: Review v3 (
Sprint-0-Plan-Review-v3.md) scored the current code at 79% and identified 13 remaining gaps. Patrick decided Path A (Q15) — finish the full extraction for v0.1.0. These 5 workstreams close every gap and project a final score of 96–97%.
flowchart LR
W8[W8: Backend completion] --> W10[W10: N1 fix + unit tests]
W9[W9: Frontend implementation] --> W10
W10 --> W11[W11: Security + IT + Envers]
W11 --> W12[W12: Polish + validation tag]
W12 --> TAG[v0.0.1 tag<br/>validate pipeline]
TAG --> V01[v0.1.0 tag<br/>publish]
13.1 W8 — Backend extraction completion
Goal: Lift-and-shift the remaining backend services + controllers from InspectFlow Sprint 14.1–14.6 so the full v0.1.0 API surface exists in code.
Branch: feature/sprint-0/w8-backend-completion
Steps:
- W8-1 Create
controller/AuthController.java— port from InspectFlow. Endpoints:POST /api/auth/login— password login (credentials → JWT pair)POST /api/auth/register— registration (gated byplate.auth.registration.enabled)POST /api/auth/refresh— stateless JWT refreshGET /api/auth/me— current user info + membershipsPOST /api/auth/logout— logout (client discards token; LoginEvent recorded)
- W8-2 Create
service/InvitationService.java+controller/InvitationController.java— port from InspectFlow 14.3. Endpoints: create invitation, accept invitation (by token), list invitations (admin), revoke invitation. Wired toInvitationMailer+OrgDisplayNameResolverSPIs. - W8-3 Create
service/AccessRequestService.java+controller/AccessRequestController.java— port from InspectFlow 14.4. Endpoints: submit access request, approve/reject request (admin), list pending requests. Wired toAccessRequestMailerSPI. - W8-4 Create
controller/AdminAuditController.java— port from InspectFlow 14.6. Enpoints: list login events, query Envers audit revisions. EnforceshasRole('ADMIN')via method-security. - W8-5 Create
filter/OrgContextResolver.java— port from InspectFlow. Servlet filter that resolves the active org context from the request path/header and stores it for downstream use. - W8-6 Update
PlateAuthAutoConfiguration@Importlist — add each new service/controller to the explicit import list. No@ComponentScan. - W8-7 Verify:
mvn -pl plate-auth-starter compilepasses with all new classes. No references tode.platesoft.inspectflow.*remain.
Done when: All endpoints respond (unit-testable with mocked repos). The membership lifecycle is reachable: Invitation → accept → Membership created; AccessRequest → approve → Membership created.
13.2 W9 — Frontend implementation
Goal: Replace the throwing stubs with real, Edge-compatible implementations. The npm package must be functional.
Branch: feature/sprint-0/w9-frontend
Steps:
- W9-1 Implement
createAuthConfig(opts)inpackages/auth/src/config/index.ts:- Build provider list from
opts.providers(Google primary; Microsoft/email conditional) signIncallback: build exchange envelope, callexchangeWithBackend(), store access token + memberships in JWTjwtcallback: persistaccess_token+membershipsfrom backend responsesessioncallback: exposeaccessTokento client (server-side only viaauth())allowDangerousEmailAccountLinking: false— verified by test
- Build provider list from
- W9-2 Implement
signEnvelope()/verifyEnvelope()inpackages/auth/src/exchange/envelope.ts:- Web Crypto API (
crypto.subtle.importKey+sign("HMAC", ...)) — runs in Node + Edge runtime makeNonce()→crypto.randomUUID()- Golden test vector shared with backend (TR-3 contract)
- Web Crypto API (
- W9-3 Implement
createProxyHandlers(opts)inpackages/auth/src/proxy/handlers.ts:auth()→Authorization: Bearer <token>injection (uses NextAuth v5auth(), NOTgetToken)duplex: "half"for streaming POST/PUT bodies- Strip hop-by-hop headers (RFC 7230)
- Never echo bearer token in error responses
- W9-4 Implement
createAuthMiddleware(opts)inpackages/auth/src/middleware/index.ts:- Returns a
NextMiddlewarewith configurablepublicPaths
- Returns a
- W9-5 Implement client hooks in
packages/auth/src/client/hooks.ts:useAccessToken(): string | nulluseMemberships(): Membership[]- Re-export
useSession,signIn,signOutfromnext-auth/react
- W9-6 Configure
tsup.config.tsfor dual ESM/CJS build targeting Node 20 + Edge runtime. Verifypnpm -F @platesoft/auth buildproducesdist/with ESM + CJS +.d.ts. - W9-7 Add 5 frontend tests (envelope sign/verify golden vector, config factory snapshot, proxy header stripping, middleware public-paths, type exports).
Done when: pnpm -F @platesoft/auth build + pnpm -F @platesoft/auth test pass. A sample Next.js app can import { createAuthConfig } from '@platesoft/auth/config' and boot without throwing.
13.3 W10 — N1 fix + unit tests
Goal: Fix the deprecated dependency (N1) and implement the 15 unit tests from the Testplan.
Branch: feature/sprint-0/w10-n1-unittests
Steps:
- W10-1 Fix N1: In
plate-auth-starter/pom.xmlline 22, renamespring-boot-starter-web→spring-boot-starter-webmvc. Verifymvn compilepasses with no deprecation warnings for this starter. - W10-2 Implement T-UT01..03 —
JwtServiceTest(IDs aligned toSprint-0-Testplan.md§3):- T-UT01: access-token generation with correct claims (
sub,email,role,iss=plate-auth,exp ≈ now+15min) - T-UT02: refresh-token generation — longer
exp(≈30d), distinctjti,type=refresh - T-UT03: invalid/tampered token →
isTokenValidreturnsfalse, no exception leaks (logged at DEBUG)
- T-UT01: access-token generation with correct claims (
- W10-3 Implement T-UT04..08 —
ExchangeServiceTest:- T-UT04:
mint(...)returns signed envelope (UUID nonce, epochiat, Base64 SHA-256 HMAC over canonical concat) - T-UT05:
consume(...)happy path withinmax-age→ returnsTokenResponse, nonce enters consumed-set - T-UT06: nonce replay within TTL → throws
ExchangeReplayException(HTTP 409), audit emitted - T-UT07: HMAC tamper (e.g.
rolemutated) → throwsExchangeHmacInvalidException, auditEXCHANGE_HMAC_FAILED - T-UT08: clock skew beyond max-age (
iat = now-70s, max-age=60s) → throwsExchangeExpiredException
- T-UT04:
- W10-4 Implement T-UT09 —
HmacEnvelopeTest(de.platesoft.auth.crypto):signThenVerifyround-trip succeeds; wrong secret returnsfalse; compare usesMessageDigest.isEqual(constant-time)
- W10-5 Implement T-UT10..11 —
MembershipServiceTest:- T-UT10:
effectiveRole(...)returns highest rank across memberships (USER in Org A, ADMIN in Org B → ADMIN) - T-UT11:
addMembership(...)with(org_type, org_id)rejected byOrgValidatorSPI → throwsOrgValidationException, no row inserted (polymorphic FK validation)
- T-UT10:
- W10-6 Implement T-UT12 —
InvitationServiceTest(blocked until W8-2 completes theInvitationService):create(...)stores only the hashed token (bcrypt/SHA-256), NOT plaintext; plaintext returned exactly once; expiration =now + 7d
- W10-7 Implement T-UT13 —
AccessRequestServiceTest(blocked until W8-3 completes theAccessRequestService):approve(...)on aPENDINGrequest → statusAPPROVED,Membershiprow created with requested role,AccessRequestMailerSPI invoked
- W10-8 Implement T-UT14 —
PlateAuthPropertiesValidationTest(de.platesoft.auth.config, parameterized):- JWT secret 31 chars → fails (
@Size(min=32)); exchange secret null → fails (@NotBlank); malformed CORS origin → fails; all-valid → passes; context fails fast withBindValidationException
- JWT secret 31 chars → fails (
- W10-9 Implement T-UT15 —
OrgContextResolverTest(de.platesoft.auth.spi, SPI fallback):- No user-provided
OrgValidator→ defaultPermissiveOrgValidatoraccepts all(org_type, org_id)and emits one WARN per call (assert WARN count == N, not throttled)
- No user-provided
Dependency note: T-UT12 and T-UT13 depend on the
InvitationService/AccessRequestServicecreated in W8-2/W8-3. They must run after W8 lands (the W8→W10 ordering already enforces this). The remaining tests (T-UT01..11, T-UT14..15) only depend on W2/W4 code that already exists.
Done when: mvn -pl plate-auth-starter test runs all 15 unit tests T-UT01..15 green, with IDs matching Sprint-0-Testplan.md §3. The existing version-constant test still passes.
13.4 W11 — Security + integration + Envers
Goal: Implement the security suite, 6 remaining integration tests, and the Envers audit infrastructure. Per the deferral above, 9 of 10 security tests are implemented in v0.1 (T-SEC01..09); T-SEC10 is explicitly skipped (v0.2 candidate).
Branch: feature/sprint-0/w11-security-it-envers
Steps:
- W11-1 Create
entity/RevInfo.java+listener/RevInfoListener.java:RevInfo— Envers revision entity withactor_user_idfieldRevInfoListener— populatesactor_user_idfromSecurityContextHolder(defensive null check)- Register
@Auditedon all state-changing entities (User, Membership, Invitation, AccessRequest)
- W11-2 Implement T-SEC01..10 (security tests, IDs aligned 1:1 to
Sprint-0-Testplan.md§7):- T-SEC01: HMAC tamper rejected — mutate
role, expect 401 + audit (same scenario as T-UT07) - T-SEC02: Nonce replay rejected within TTL — second
consumesame envelope → 409 (same scenario as T-UT06) - T-SEC03: Envelope rejected after max-age —
iatskewed +70s (max-age=60s), expect 401 (same scenario as T-UT08) - T-SEC04: Expired JWT rejected —
exp = now-1s, expect 401, no SecurityContext populated - T-SEC05: Missing JWT secret fails startup —
plate.auth.jwt.secretunset →BindValidationException(@NotBlank) - T-SEC06: Short JWT secret fails startup —
plate.auth.jwt.secret=tooShort(8 chars) →@Size(min=32)violated - T-SEC07: CORS unknown origin rejected — preflight from
https://attacker.examplereturns noAccess-Control-Allow-Origin - T-SEC08: SQL injection probe on
/auth/loginrejected —' OR 1=1 --→ 401, no SQL leak (JPA binding, not concat) - T-SEC09: Constant-time HMAC compare — static check
MessageDigest.isEqual+ microbenchmark timing variance < 5% - T-SEC10: ⏭️ SKIPPED / DEFERRED for v0.1 — Refresh-token rotation is a v0.2 candidate per
Sprint-0-Testplan.md§7 andRoadmap.md. TheRefreshTokenentity was deleted in the B5 fix; v0.1 uses stateless JWT refresh and accepts refresh-token re-use as-is. Do NOT implement this in W11. Record the deferral in the v0.1.0 release notes and the Open-Questions log (it becomes a v0.2 task). W11 therefore delivers 9 implemented security tests (T-SEC01..09) + 1 documented skip (T-SEC10).
- T-SEC01: HMAC tamper rejected — mutate
- W11-3 Implement T-IT04..09 (integration tests with Testcontainers Postgres):
- IT04: JWT filter IT — full request flow with valid/invalid/expired token
- IT05: Membership repository IT — CRUD against real Postgres
- IT06: Invitation flow IT — create → accept → membership created end-to-end
- IT07: Access request flow IT — submit → approve → membership created end-to-end
- IT08: Login audit IT — LoginEvent recorded on success + failure, Envers revision has
actor_user_id - IT09: SPI swap IT — consumer provides custom
OrgValidator→ starter uses it instead of default
- W11-4 Switch integration tests from H2 to Testcontainers Postgres so Flyway migrations run against the real target database (gap #13).
Done when: mvn -pl plate-auth-starter verify + mvn -pl it verify runs all security + integration tests against Testcontainers Postgres, all green. Envers revisions have actor_user_id populated.
13.5 W12 — Polish + validation
Goal: Close the remaining doc/quality gaps, resolve Open Questions, and validate the publish pipeline end-to-end.
Branch: feature/sprint-0/w12-polish-validation
Steps:
- W12-1 W-B fix — Flyway co-existence test: create an IT where the consumer app has its own primary Flyway (
flyway_schema_history) + plate-auth's Flyway (flyway_schema_history_auth) both run. Assert plate-auth's migrations run independently and before JPA init. - W12-2 W-D fix — Align
CHANGELOG.md+README.mdto the actual v0.1.0 shipped surface. Document N1 (consumer needsspring-boot-starter-flywayif relying on Flyway auto-config). - W12-3 Resolve Open Questions — Patrick locks the 6 remaining 🟡 questions before the v0.1.0 tag:
- Q02: MS Entra ID → defer to v0.2 (leaning confirmed)
- Q03: Flyway separate history table (leaning confirmed — already implemented)
- Q06: SemVer lockstep + wire-version constant (implement wire-version in W9)
- Q07: Gitea Actions publish on
v*tag (leaning confirmed — W6 pipeline) - Q09: Frontend bundler → ✅ tsup decided —
packages/auth/tsup.config.tsalready exists; W9 implements the dual ESM/CJS build - Q12: Audit DB-only in v0.1 (leaning confirmed)
- W12-4 Cut
v0.0.1validation tag — tag from the W12 branch after all tests pass. This validates the publish pipeline end-to-end:- Maven:
de.platesoft:plate-auth-starter:0.0.1→ Gitea Maven Registry - npm:
@platesoft/auth@0.0.1→ Gitea npm Registry - Consume in a throwaway test app: boot,
/api/auth/config, Google sign-in,/api/auth/me
- Maven:
- W12-5 After
v0.0.1validates cleanly, cutv0.1.0from the same commit.
Done when: v0.0.1 tag publishes both artifacts to the Gitea Package Registry. mvn dependency:get de.platesoft:plate-auth-starter:0.0.1 + npm view @platesoft/auth@0.0.1 both succeed from a fresh machine.
13.6 Phase 2 — completion summary
| Workstream | Closes gaps | Key deliverables |
|---|---|---|
| W8 | #1 (B3) | AuthController, Invitation/AccessRequest services+controllers, AdminAuditController, OrgContextResolver |
| W9 | #2, #6 | createAuthConfig, signEnvelope, createProxyHandlers, middleware, hooks, tsup build, 5 frontend tests |
| W10 | #3, #7 (N1) | Rename web starter, 15 unit tests (T-UT01..15) |
| W11 | #4, #5, #8 (R3), #13 | RevInfo entity, 10 security tests, 6 ITs, Testcontainers Postgres |
| W12 | #9 (W-B), #10 (W-D), #11, #12 | Flyway co-existence test, CHANGELOG/README, resolve Open Qs, v0.0.1 tag |
Estimated total: ~6–7 engineering days. Projected score after completion: 96–97%.
14. Cross-references
- Assessment:
Sprint-0-Assessment.md - Test plan:
Sprint-0-Testplan.md - Architecture reference:
Architecture.md - Open questions:
Open-Questions.md - Consumer integration:
Integration-Guide.md - InspectFlow migration recipe:
Migration-InspectFlow.md - Plan Review v3:
Sprint-0-Plan-Review-v3.md(current — score 79%, Path A endorsed)
End of plan v2.
Status: Phase 1 (W1–W7) partially complete. Phase 2 (W8–W12) endorsed by Review v3 @ 79%. Ready for Code mode execution. Patrick GO for Path A — full v0.1.0 extraction.