plan: add Integration-Guide.md (greenfield Sparkboard 5-min setup)

Patrick Plate
2026-06-24 14:29:12 +02:00
parent 09a84bf0c8
commit e6ba33b769
+453
@@ -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 (`<AuthProvider>`, `<LoginForm>`, `<SignupForm>`).
- **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
<dependency>
<groupId>de.platesoft</groupId>
<artifactId>plate-auth-starter</artifactId>
<version>0.1.0</version>
</dependency>
```
Configure the Gitea registry in `pom.xml`:
```xml
<repositories>
<repository>
<id>gitea-platesoft</id>
<url>https://git.plate-software.de/api/packages/platesoft/maven</url>
</repository>
</repositories>
```
Add credentials to `~/.m2/settings.xml`:
```xml
<server>
<id>gitea-platesoft</id>
<username>your-gitea-user</username>
<password>your-gitea-token</password>
</server>
```
### 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 <LoginForm onSuccess={() => 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 (
<html>
<body>
<AuthProvider session={session}>{children}</AuthProvider>
</body>
</html>
);
}
```
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<String, String> 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 <h1>Hello, {hello}</h1>;
}
```
---
## 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).**