diff --git a/Integration-Guide.md b/Integration-Guide.md new file mode 100644 index 0000000..a89195a --- /dev/null +++ b/Integration-Guide.md @@ -0,0 +1,453 @@ +# Integration Guide + +**Status:** Draft v1 +**Date:** 2026-06-24 +**Owner:** Patrick +**Audience:** Greenfield Spring Boot 4.1 + Next.js 15 App Router consumers (primary target: Sparkboard) +**Library version:** `0.1.0` + +> If you are migrating an existing app that already has its own auth (InspectFlow), see [Migration-InspectFlow.md](Migration-InspectFlow.md) instead. + +--- + +## 1. What you get + +By adding `de.platesoft:plate-auth-starter:0.1.0` (backend) + `@platesoft/auth:0.1.0` (frontend) to your project, you get: + +- **Backend:** REST endpoints for login, signup (opt-in), password reset, OAuth (Google), token exchange (HMAC-signed envelope), JWT-protected filter, audit, memberships, invitations, access requests. +- **Frontend:** NextAuth v5 config factory, edge-runtime API proxy, HMAC envelope sign/verify helpers, optional React components (``, ``, ``). +- **Schema:** Five Postgres tables created via Flyway in a private `flyway_schema_history_auth` table. +- **Extension points (SPI):** `OrgValidator`, `OrgDisplayNameResolver`, `InvitationMailer`, `AccessRequestMailer`, `OnboardingHook` — implement to plug your domain in without forking. + +--- + +## 2. Prerequisites + +| Item | Required | Notes | +|------|----------|-------| +| Java | 25 LTS | Matches Spring Boot 4.1 minimum | +| Spring Boot | 4.1.0+ | 4.0.x not supported (see [Open-Questions.md](Open-Questions.md) Q08) | +| Postgres | 14+ (16 recommended) | Or compatible (e.g. CockroachDB — untested) | +| Node | 20+ (22 LTS recommended) | NextAuth v5 + Edge runtime | +| Next.js | 15+ App Router | Pages Router not supported | +| Gitea Maven registry access | Yes | URL + token in `~/.m2/settings.xml` | +| Gitea npm registry access | Yes | `.npmrc` with `@platesoft:registry=...` | + +--- + +## 3. Five-minute setup (backend) + +### 3.1 Add Maven dependency + +```xml + + de.platesoft + plate-auth-starter + 0.1.0 + +``` + +Configure the Gitea registry in `pom.xml`: + +```xml + + + gitea-platesoft + https://git.plate-software.de/api/packages/platesoft/maven + + +``` + +Add credentials to `~/.m2/settings.xml`: + +```xml + + gitea-platesoft + your-gitea-user + your-gitea-token + +``` + +### 3.2 Required configuration (application.yml) + +```yaml +plate: + auth: + jwt: + secret: ${PLATE_AUTH_JWT_SECRET} # 32+ chars, hex/base64 + access-expiration: PT15M + refresh-expiration: P30D + issuer: my-app + exchange: + secret: ${PLATE_AUTH_EXCHANGE_SECRET} # 32+ chars, distinct from jwt secret + max-age: PT60S + nonce-ttl: PT5M + registration: + enabled: false # invite-only by default + cors: + allowed-origins: + - https://app.example.com + providers: + google: + enabled: true + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/myapp + username: ${DB_USER} + password: ${DB_PASSWORD} +``` + +That's it for the bare minimum. Run your app — the starter auto-configures. + +### 3.3 What auto-config wires for you + +| Bean | Purpose | +|------|---------| +| `JwtService` | Mints + parses JWTs | +| `ExchangeService` | HMAC envelope mint/consume | +| `JwtAuthenticationFilter` | Reads `Authorization: Bearer ...`, populates `SecurityContext` | +| `SecurityFilterChain` (named `plateAuthSecurityChain`) | Whitelists `/auth/**`, requires JWT elsewhere | +| `OrgValidator` (default = `PermissiveOrgValidator`) | Accepts any `(org_type, org_id)` — replace via SPI | +| `InvitationMailer` (default = `LoggingMailer`) | Logs mails to console — replace via SPI | +| `AccessRequestMailer` (default = `LoggingMailer`) | Same | +| `OnboardingHook` (default = `NoopOnboardingHook`) | Does nothing — replace via SPI | + +All defaults are `@ConditionalOnMissingBean` — define your own bean of the same type to override. + +### 3.4 Plug in your domain (SPI seams) + +```java +@Configuration +public class PlateAuthSpiConfig { + + @Bean + public OrgValidator orgValidator(StudioRepository repo) { + return (orgType, orgId) -> + "STUDIO".equals(orgType) && repo.existsById(orgId); + } + + @Bean + public OrgDisplayNameResolver orgDisplayNameResolver(StudioRepository repo) { + return (orgType, orgId) -> repo.findById(orgId) + .map(Studio::getName) + .orElse("Unknown"); + } + + @Bean + public InvitationMailer invitationMailer(MailService mail) { + return (toEmail, inviter, orgName, token) -> + mail.send(toEmail, "Invite to " + orgName, "Open: https://app.example.com/invite/" + token); + } + + @Bean + public OnboardingHook onboardingHook(StudioService studios) { + return (userId, email, signupContext) -> { + // e.g. create a personal default studio for new sign-ups + studios.createForUser(userId, email); + }; + } +} +``` + +### 3.5 Protect your endpoints + +By default, anything outside `/auth/**` requires a valid JWT. To customize: + +```java +@Bean +@Order(0) // before plateAuthSecurityChain +public SecurityFilterChain myCustomChain(HttpSecurity http) throws Exception { + return http + .securityMatcher("/public/**") + .authorizeHttpRequests(a -> a.anyRequest().permitAll()) + .build(); +} +``` + +To read the current user in a controller: + +```java +@GetMapping("/api/me") +public MeResponse me(@AuthenticationPrincipal Jwt principal) { + return new MeResponse(principal.getSubject(), principal.getClaim("email")); +} +``` + +--- + +## 4. Five-minute setup (frontend) + +### 4.1 Add npm dependency + +```bash +# .npmrc must contain: +# @platesoft:registry=https://git.plate-software.de/api/packages/platesoft/npm/ +# //git.plate-software.de/api/packages/platesoft/npm/:_authToken=YOUR_TOKEN + +pnpm add @platesoft/auth@0.1.0 +``` + +### 4.2 Required env vars + +```bash +# .env.local +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=... # NextAuth v5 session secret, 32+ chars +PLATE_AUTH_EXCHANGE_SECRET=... # MUST match backend plate.auth.exchange.secret +PLATE_AUTH_BACKEND_URL=http://localhost:8080 +GOOGLE_CLIENT_ID=... +GOOGLE_CLIENT_SECRET=... +``` + +### 4.3 Wire NextAuth via factory + +Create `frontend/auth.ts`: + +```typescript +import { createAuthConfig } from "@platesoft/auth/server"; +import NextAuth from "next-auth"; + +export const { handlers, auth, signIn, signOut } = NextAuth( + createAuthConfig({ + backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, + exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!, + nextAuthSecret: process.env.NEXTAUTH_SECRET!, + providers: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, + pages: { + signIn: "/login", + }, + }) +); +``` + +### 4.4 NextAuth route handler (boilerplate, required) + +Create `frontend/app/api/auth/[...nextauth]/route.ts`: + +```typescript +import { handlers } from "@/auth"; +export const { GET, POST } = handlers; +``` + +> **Note:** NextAuth v5 requires this exact filename for its OAuth callback URLs to work. Do not rename it. This is a NextAuth limitation, not ours — see [Roadmap.md](Roadmap.md) for "remove boilerplate" v0.2 ambition. + +### 4.5 API proxy (forward authenticated requests to backend) + +Create `frontend/app/api/[...path]/route.ts`: + +```typescript +import { createProxyHandlers } from "@platesoft/auth/edge"; + +export const runtime = "edge"; + +export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({ + backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, + exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!, +}); +``` + +### 4.6 Optional React components + +```tsx +// app/login/page.tsx +import { LoginForm } from "@platesoft/auth/react"; + +export default function LoginPage() { + return window.location.href = "/dashboard"} />; +} +``` + +```tsx +// app/layout.tsx +import { AuthProvider } from "@platesoft/auth/react"; +import { auth } from "@/auth"; + +export default async function RootLayout({ children }) { + const session = await auth(); + return ( + + + {children} + + + ); +} +``` + +If you want to ship your own UI, skip the React import — only `@platesoft/auth/server` + `@platesoft/auth/edge` are required. + +--- + +## 5. Database setup + +The starter ships Flyway migrations under classpath:`db/migration/plate-auth/`. They run automatically on startup against a **dedicated history table** (`flyway_schema_history_auth`). + +If your app also uses Flyway with its own migrations: + +```yaml +spring: + flyway: + enabled: true # your migrations, your history table + locations: classpath:db/migration + table: flyway_schema_history # YOUR table +``` + +The starter's Flyway bean (`plateAuthFlyway`) runs **before** the application's main Flyway: + +| Order | Bean | Locations | History table | +|-------|------|-----------|---------------| +| 1 | `plateAuthFlyway` | `db/migration/plate-auth` | `flyway_schema_history_auth` | +| 2 | (your default Flyway) | `db/migration` | `flyway_schema_history` | + +You don't need to configure the starter's Flyway — it's wired automatically. + +### 5.1 What's created + +| Table | Purpose | +|-------|---------| +| `users` | Identity record + password hash (nullable for OAuth-only) | +| `user_identities` | Provider linkage (google, password, ms-entra) | +| `memberships` | `(user_id, org_type, org_id, role)` — polymorphic FK | +| `invitations` | Token-hashed invitations | +| `access_requests` | Self-service access requests | +| `login_events` | Audit (success/failure, IP, UA) | + +See [Architecture.md § 6 ER diagram](Architecture.md) for the schema. + +--- + +## 6. Minimum viable consumer app — full example + +A working **hello world** consumer: + +### Backend (`SparkboardApplication.java`) + +```java +@SpringBootApplication +public class SparkboardApplication { + public static void main(String[] args) { + SpringApplication.run(SparkboardApplication.class, args); + } +} + +@RestController +class HelloController { + @GetMapping("/api/hello") + public Map hello(@AuthenticationPrincipal Jwt principal) { + return Map.of("hello", principal.getClaim("email")); + } +} +``` + +That's it. Add the dependency, set the 4 env vars, and `/api/hello` is JWT-protected. + +### Frontend (`app/page.tsx`) + +```tsx +import { auth } from "@/auth"; +import { redirect } from "next/navigation"; + +export default async function Home() { + const session = await auth(); + if (!session) redirect("/login"); + + const res = await fetch("http://localhost:3000/api/hello", { + headers: { Cookie: (await import("next/headers")).cookies().toString() }, + }); + const { hello } = await res.json(); + return

Hello, {hello}

; +} +``` + +--- + +## 7. Operational considerations + +### 7.1 Secrets + +| Secret | Length | Rotation strategy (v0.1) | +|--------|--------|-------------------------| +| `plate.auth.jwt.secret` | ≥32 chars | Rotate → all sessions invalidated. Plan downtime. | +| `plate.auth.exchange.secret` | ≥32 chars | Rotate → all in-flight envelopes rejected. Coordinate rolling deploy with frontend. | +| `NEXTAUTH_SECRET` | ≥32 chars | Rotate → all NextAuth sessions invalidated. | + +v0.2 will add multi-key support (key-id header). For v0.1: rotate during planned maintenance. + +### 7.2 Logging + +Set `logging.level.de.platesoft.auth=INFO` in production. `DEBUG` reveals envelope details (without secrets) — useful for troubleshooting but verbose. + +### 7.3 Replication / scale-out + +v0.1 nonce store is **in-memory** (`ConcurrentHashMap`). Replicas do not share the nonce set. Consequence: if the frontend sends the same envelope to a different replica, the replay-detection misses. Mitigations: + +- Sticky sessions (load balancer affinity by user) +- Single replica (acceptable for v0.1 consumers) +- Wait for v0.2 `NonceStore` SPI with Redis backend + +### 7.4 Migrations on existing DBs + +For greenfield (Sparkboard): nothing special. For existing DBs with conflicting tables (e.g. you already have `users`): rename your tables or talk to Patrick — there's no `tablePrefix` option in v0.1 (see [Roadmap.md](Roadmap.md) v0.2). + +--- + +## 8. Verification checklist (after first deploy) + +Run these manually to verify the integration is healthy: + +- [ ] App boots without errors. Logs show `Started ... in X seconds`. +- [ ] `curl http://localhost:8080/auth/health` returns 200 OK. +- [ ] DB has 5 new tables under your schema. `flyway_schema_history_auth` has 5 rows. +- [ ] `curl -d '{"email":"a","password":"b"}' http://localhost:8080/auth/login` returns 401 with structured JSON error (not a stack trace). +- [ ] Frontend `/login` page renders. +- [ ] Sign-in with Google works → redirect → `/api/hello` returns email. +- [ ] No `WARN` logs about missing SPI defaults (you should have replaced `PermissiveOrgValidator` and `LoggingMailer`). + +--- + +## 9. Common pitfalls + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `BindValidationException: jwt.secret` at boot | Secret missing or < 32 chars | Set env var | +| 401 on every `/api/...` call from frontend | `exchangeSecret` mismatch between FE and BE | Verify both sides read same secret | +| `OAuth: redirect_uri_mismatch` from Google | Google console not updated with your URL | Add `https://app.example.com/api/auth/callback/google` to Google OAuth client | +| `409 nonce already used` on every login | Sticky sessions disabled, multiple replicas | Use single replica or wait for v0.2 NonceStore | +| Flyway: "schema not empty" on a fresh DB | Your existing tables collide with `users`, etc. | See [Migration-InspectFlow.md](Migration-InspectFlow.md) for the existing-DB recipe | +| `@AuthenticationPrincipal` returns null | Filter chain order — your custom chain ran before `plateAuthSecurityChain` and short-circuited | Set `@Order` on your custom chain explicitly | + +--- + +## 10. Next steps after first integration + +1. Replace `PermissiveOrgValidator` with a real one (your org table). +2. Replace `LoggingMailer` with `JavaMailSender` + your SMTP config. +3. Add `OnboardingHook` to create default resources for new users. +4. Subscribe to [Roadmap.md](Roadmap.md) v0.2 — refresh-token rotation + magic-link + multi-key secrets. +5. Read [Open-Questions.md](Open-Questions.md) to understand which decisions might shift in v0.2. + +--- + +## 11. Cross-references + +- [Home.md](Home.md) +- [Vision.md](Vision.md) +- [Architecture.md](Architecture.md) +- [Roadmap.md](Roadmap.md) +- [Sprint-0-Assessment.md](Sprint-0-Assessment.md) +- [Sprint-0-Plan.md](Sprint-0-Plan.md) +- [Sprint-0-Testplan.md](Sprint-0-Testplan.md) +- [Open-Questions.md](Open-Questions.md) +- [Migration-InspectFlow.md](Migration-InspectFlow.md) + +--- + +**End of Integration-Guide.md (v1).**