Table of Contents
- Sprint 1 — Implementation Plan
- Reading guide
- 1. Scope + ground rules
- 1.1 In scope (Sprint 1 / v0.2.0)
- 1.2 Out of scope (deferred — see Roadmap)
- 1.3 Stretch (only if must-haves + nice-to-haves finish early)
- 1.4 Ground rules
- 2. Workstream overview
- 3. W1 — MS Entra ID provider
- 4. W2 — Email magic-link provider
- 5. W3 — LoginEventSink SPI + real mailers
- 5.1 LoginEventSink SPI
- 5.2 Real InvitationMailer
- 5.3 AccessRequestMailer admin template
- 5.4 Mail config additions
- 6. W4 — RFC 7807 Problem Details + configurable expiration
- 7. W5 — TypeScript improvements + Edge-runtime safety
- 8. W6 — Wire-version assessment + integration tests + publish v0.2.0
- 9. Security review checklist additions
- 9.1 MS Entra ID provider [v0.2-new]
- 9.2 Email magic-link [v0.2-new]
- 9.3 LoginEventSink SPI [v0.2-new]
- 9.4 Real mailers [v0.2-new]
- 9.5 RFC 7807 Problem Details [v0.2-new]
- 9.6 Wire-version [v0.2-new] (only if bumped)
- 10. Rollout plan
- 11. Acceptance criteria
- 12. Items deferred to v0.3+
- 13. Cross-references
Sprint 1 — Implementation Plan
Status: Draft v1
Date: 2026-06-24
Owner: Patrick (plate-software)
Based on: Sprint-1-Assessment.md, Architecture.md, Roadmap.md v0.2 section
Target version: 0.2.0 (both Maven + npm, lockstep)
Theme: Polish — make the consumer experience pleasant
Reading guide
This plan follows the same structure as Sprint-0-Plan.md:
- Scope + ground rules (this section)
- Workstream overview (6 workstreams, W1–W6)
- Step-by-step per workstream (goal, steps, done-when)
- Security review checklist additions (incremental over v0.1's §9)
- Rollout + acceptance criteria
Each step is numbered so the Code-mode worker can check off progress without ambiguity.
1. Scope + ground rules
1.1 In scope (Sprint 1 / v0.2.0)
- W1 — MS Entra ID provider (backend OAuth + frontend NextAuth provider)
- W2 — Email magic-link provider (backend verification + frontend NextAuth Email provider + default UX page)
- W3 —
LoginEventSinkSPI + realInvitationMailer(JavaMailSender) +AccessRequestMaileradmin template - W4 — RFC 7807 Problem Details on
/api/auth/*+ configurable invitation expiration - W5 — TypeScript improvements (typed exports, Zod schemas) + Edge-runtime safe
useAccessToken() - W6 — Wire-version assessment + integration tests + publish
v0.2.0
1.2 Out of scope (deferred — see Roadmap)
- Multi-replica nonce store (v0.3)
- Refresh-token rotation table (v0.3)
- JWT secret rotation via
kid(v0.3) - WebAuthn / passkeys (possibly v0.2 — see §1.3)
- Per-app branding on exchange endpoint (possibly v0.2 — see §1.3)
- SAML, SCIM, OIDC server, mobile SDKs (post-1.0)
1.3 Stretch (only if must-haves + nice-to-haves finish early)
- WebAuthn / passkey provider — no consumer has asked; defer unless explicitly requested
- Per-app branding — low value for a 2-consumer library; defer
- ADR-style docs auto-published to wiki
1.4 Ground rules
- v0.2 is additive. No removal of v0.1 public API. Any breaking change is documented in CHANGELOG with a migration recipe (per deprecation policy).
- Lockstep versions.
@platesoft/auth@0.2.0andde.platesoft:plate-auth-starter:0.2.0ship together from the samev0.2.0git tag. - Wire-version discipline. If the exchange envelope contract changes, bump
WIRE_VERSIONto 2. If it doesn't (providers fit existing shape),WIRE_VERSIONstays at 1 and v0.2 is purely additive. - Branch policy:
feature/sprint-1/<workstream>branches → PR → squash-merge tomain. - GLM-5.2 + Lumen working model. Effort estimates reflect AI-assisted speed — the provider implementations follow the same OAuth pattern as Google (already proven in v0.1).
2. Workstream overview
flowchart LR
W1[W1: MS Entra ID provider] --> W6[W6: Wire-version<br/>+ IT + publish]
W2[W2: Email magic-link] --> W6
W3[W3: LoginEventSink SPI<br/>+ real mailers] --> W6
W4[W4: RFC 7807 Problem Details<br/>+ config expiration] --> W6
W5[W5: TS improvements<br/>+ Edge safety] --> W6
W6 --> TAG[v0.2.0 tag]
| # | Workstream | Priority | Depends on | Est. (AI-assisted) |
|---|---|---|---|---|
| W1 | MS Entra ID provider (backend + frontend) | Must-have | v0.1 shipped | ~1 day |
| W2 | Email magic-link provider | Nice-to-have | v0.1 shipped | ~1 day |
| W3 | LoginEventSink SPI + real mailers | Must-have (mailers) / Nice (SPI) | v0.1 shipped | ~1.5 days |
| W4 | RFC 7807 + configurable expiration | Must-have | v0.1 shipped | ~0.5 days |
| W5 | TS improvements + Edge safety | Nice-to-have | v0.1 shipped | ~1 day |
| W6 | Wire-version + IT + publish v0.2.0 | Required | W1–W5 | ~1 day |
| Total | ~6 days |
W1–W5 are parallelizable. W6 is the integration + publish gate.
3. W1 — MS Entra ID provider
Goal: Add Microsoft Entra ID as a first-class OAuth provider, symmetric with Google. This fixes
the regression where InspectFlow's existing provider=microsoft identities have no sign-in path in
v0.1.
Branch: feature/sprint-1/w1-ms-entra
3.1 Backend
Steps:
- W1-1 Create
service/MicrosoftEntraService.java(or extendOAuthService) to handle MS Entra ID token verification. Same pattern as Google:- Accept
id_tokenfrom the frontend exchange envelope - Verify signature against MS Entra JWKS endpoint
- Extract
oid(Microsoft's stable user id),email,name,tid(tenant id) - Find-or-create
User+UserIdentity(provider=MICROSOFT, subject=oid, tenant_id=tid) - Call
OnboardingHook.onFirstSignIn(...)for new users
- Accept
- W1-2 Update
LoginProviderenum — addMICROSOFT(if not already present from v0.1'semailtoggle). The enum is already provider-agnostic. - W1-3 Add
Microsoftprovider config toPlateAuthProperties.Providers:@Data public static class Microsoft { private boolean enabled = false; private String tenantId = "common"; // "common", "organizations", or a specific GUID } - W1-4 The exchange envelope already has a
providerfield. MS Entra payloads useprovider="microsoft". No envelope schema change needed — the existingExchangeEnvelopeis provider-agnostic. - W1-5 Register the provider conditionally:
Fail-fast if enabled without
@Bean @ConditionalOnProperty(prefix = "plate.auth.providers.microsoft", name = "enabled", havingValue = "true") public MicrosoftEntraService microsoftEntraService(...) { ... }spring.security.oauth2.client.registration.microsoft.*configured.
Done when: A POST to /api/auth/exchange with provider=microsoft and a valid MS Entra
id_token returns {access_token, refresh_token, user, memberships}. Existing Google flow is
unaffected (T-IT10).
3.2 Frontend
Steps:
- W1-6 Implement
packages/auth/src/config/providers/microsoft.ts:Uses NextAuth v5'sexport function microsoftProvider(opts: { clientId: string; clientSecret: string; tenantId?: string; // default "common" }): NextAuthConfig.Provider;MicrosoftEntraIDprovider. - W1-7 Wire into
createAuthConfig(opts)— addmicrosoft?: MicrosoftOptstoPlateAuthConfigOptions.providers. If present, add the MS provider to the provider list. - W1-8 Update
ExchangeEnvelopetype — theproviderunion type becomes'google' | 'microsoft' | 'email' | 'password'(already in v0.1's type, now exercised). - W1-9 Document env vars in Architecture §4.2:
MICROSOFT_ENTRA_CLIENT_ID=... MICROSOFT_ENTRA_CLIENT_SECRET=... MICROSOFT_ENTRA_TENANT_ID=common
Done when: A Next.js app with createAuthConfig({ providers: { microsoft: {...} } }) shows a
"Sign in with Microsoft" button and completes the OAuth flow → exchange → JWT. Snapshot test
verifies the provider is present in the config (T-FE06).
3.3 InspectFlow regression test
- W1-10 Verify against InspectFlow's seeded MS identities. The V5 migration
(
V5__add_microsoft_tenant_id_index.sql) already indexesuser_identities.microsoft_tenant_id. A user withprovider=microsoft, subject=<oid>, tenant_id=<tid>must sign in successfully and get their existing memberships.
Done when: InspectFlow MS user sign-in is green post-v0.2.0 migration. T-E2E07 validates this.
4. W2 — Email magic-link provider
Goal: Add email magic-link sign-in as a second auth option. Users enter their email, receive a one-time link, click it, and are authenticated. No password required.
Branch: feature/sprint-1/w2-email-magic-link
4.1 Backend
Steps:
- W2-1 The exchange envelope already supports
provider="email". The backendExchangeService.consume(...)path is provider-agnostic — it verifies the HMAC envelope and find-or-creates the user. No backend changes needed for the exchange itself. - W2-2 Add email-provider config to
PlateAuthProperties.Providers:@Data public static class EmailMagicLink { private boolean enabled = false; private String fromAddress; // e.g., "noreply@plate-software.de" @Min(60) private int tokenTtlSeconds = 600; // 10 min magic-link TTL } - W2-3 The magic-link flow is handled by NextAuth's Email provider on the frontend side — it sends the magic link email via SMTP. The backend's role is to receive the verified identity via the exchange envelope (same as Google). No new backend endpoint.
Done when: Backend accepts provider="email" envelopes. Config validation fails-fast if
email-magic-link.enabled=true without SMTP config.
4.2 Frontend
Steps:
- W2-4 Implement
packages/auth/src/config/providers/email.ts:Uses NextAuth v5'sexport function emailProvider(opts: { server?: SMTPConfig; // or connection string from: string; }): NextAuthConfig.Provider;Emailprovider (nodemailer-backed). - W2-5 Wire into
createAuthConfig(opts)— addemail?: EmailOptstoPlateAuthConfigOptions.providers. - W2-6 Create a default magic-link callback page component that consumers can drop in:
packages/auth/src/components/MagicLinkCallback.tsx. This handles the NextAuth verify-callback route. Consumers import it or build their own — documented in Integration-Guide.md. - W2-7 Security: verify
allowDangerousEmailAccountLinking=falseis enforced in the email provider config. Snapshot test (T-FE07).
Done when: A Next.js app with createAuthConfig({ providers: { email: {...} } }) shows an email
input, sends a magic link, and the callback completes the sign-in → exchange → JWT flow.
4.3 Email enumeration guard
- W2-8 The email-provider response must be generic: "If an account exists for this email, a sign-in link has been sent." No "user not found" error. This prevents email enumeration. Test: T-SEC12.
Done when: Magic-link request for a non-existent email returns the same response as an existing email. No information leak.
5. W3 — LoginEventSink SPI + real mailers
Goal: Two deliverables bundled here because they share the SPI/mailer architecture:
LoginEventSinkSPI — new extension point for consumers to tee login events to external systems (Loki, SIEM, webhooks) instead of (or in addition to) DB rows.- Real
InvitationMailer+AccessRequestMailer— replace v0.1's no-op loggers withJavaMailSender-backed implementations that actually send email.
Branch: feature/sprint-1/w3-spi-mailers
5.1 LoginEventSink SPI
Steps:
- W3-1 Create the SPI interface under
de.platesoft.auth.spi:public interface LoginEventSink { /** * Called after a login event is recorded. Implementations may ship to * Loki, OpenSearch, Kafka, or any external sink. Must be non-blocking — * called asynchronously by LoginEventService. * * @param event the recorded login event (never null) */ void emit(LoginEvent event); } - W3-2 Create the default no-op implementation:
@Component @ConditionalOnMissingBean(LoginEventSink.class) public class NoOpLoginEventSink implements LoginEventSink { @Override public void emit(LoginEvent event) { // no-op — default. Override to ship externally. } } - W3-3 Wire
LoginEventSinkintoLoginEventService:- After
loginEventRepository.save(event), callloginEventSink.emit(event)asynchronously (@Asyncor a dedicated executor). - The sink must not block the login flow. A failing sink logs a WARN but never fails the login.
- Wrap the
emit()call in a try-catch that logs at WARN level on failure.
- After
- W3-4 Register the SPI in
PlateAuthAutoConfiguration@Importlist. - W3-5 Update Architecture §3.4 SPI table — add
LoginEventSinkas the 6th SPI. Default =NoOpLoginEventSink.
Done when: A consumer can @Bean a LoginEventSink and receive every login event. The default
no-op sink has zero overhead. Login latency is unaffected (async dispatch). T-UT16 validates.
5.2 Real InvitationMailer
Steps:
- W3-6 Create
de.platesoft.auth.spi.defaults.MailInvitationMailer:@Component @ConditionalOnMissingBean(InvitationMailer.class) @ConditionalOnProperty(prefix = "plate.auth.mail", name = "enabled", havingValue = "true") public class MailInvitationMailer implements InvitationMailer { private final JavaMailSender mailSender; private final PlateAuthProperties properties; @Override public void sendInvitation(Invitation invitation, String acceptUrl) { MimeMessage msg = mailSender.createMimeMessage(); // build HTML + plain-text multipart from template // subject: "You're invited to join {orgDisplayName}" // body: link to acceptUrl, expiration info mailSender.send(msg); } } - W3-7 The default
InvitationMailerwhenplate.auth.mail.enabled=false(or unset) remains the existingLoggingInvitationMailer. The newMailInvitationMaileractivates only when mail is explicitly enabled. This preserves v0.1's "boots green with zero config" guarantee. - W3-8 Create email templates:
src/main/resources/templates/invitation.html— HTML body with accept-url placeholdersrc/main/resources/templates/invitation.txt— plain-text fallback- Use Spring's
MimeMessageHelperfor multipart.
- W3-9 Fail-loudly: if
plate.auth.mail.enabled=truebutspring.mail.hostis unset, fail fast at startup with a clear error:"plate.auth.mail.enabled=true but spring.mail.host is not configured".
Done when: With plate.auth.mail.enabled=true + SMTP config, inviting a user sends a real email
with the accept link. Without mail config, the logging fallback activates. T-UT17 + T-IT11 validate.
5.3 AccessRequestMailer admin template
Steps:
- W3-10 Create
de.platesoft.auth.spi.defaults.MailAccessRequestMailer— same pattern asMailInvitationMailer. Activates withplate.auth.mail.enabled=true. - W3-11 Two notification methods:
notifyAdmins(request)— sends to configured admin email listnotifyRequester(request)— sends approval/denial notification to the requester
- W3-12 Templates:
templates/access-request-admin.html— "New access request from {email}"templates/access-request-decision.html— "Your access request was {approved/denied}"
- W3-13 Admin email list from config:
plate.auth.mail.admin-recipients(comma-separated).
Done when: Access request submission → admins notified. Approval/denial → requester notified. T-UT18 validates.
5.4 Mail config additions
Add to PlateAuthProperties:
@Data public static class Mail {
private boolean enabled = false;
private String fromAddress = "noreply@plate-software.de";
private List<String> adminRecipients = new ArrayList<>();
}
plate:
auth:
mail:
enabled: false # opt-in; default is logging fallback
from-address: noreply@plate-software.de
admin-recipients:
- admin@plate-software.de
6. W4 — RFC 7807 Problem Details + configurable expiration
Goal: Two small must-haves bundled:
- RFC 7807 Problem Details — structured error responses (
application/problem+json) on all/api/auth/*endpoints so consumers can build robust error UX. - Configurable invitation expiration —
plate.auth.invitation.expiration-days(currently hardcoded 7d).
Branch: feature/sprint-1/w4-problem-details-config
6.1 RFC 7807 Problem Details
Steps:
- W4-1 Create
config/PlateAuthProblemDetailHandler— extends Spring Boot 4'sProblemDetailExceptionHandler(Spring 6 / Boot 4 has built-in RFC 7807 support via@ControllerAdvice+ProblemDetail). Wire it as a@ControllerAdvicefor thede.platesoft.auth.controllerpackage. - W4-2 Define custom problem types for plate-auth-specific errors:
public enum PlateAuthProblems { EXCHANGE_HMAC_INVALID("https://plate-software.de/errors/exchange-hmac-invalid", 401), EXCHANGE_EXPIRED("https://plate-software.de/errors/exchange-expired", 401), EXCHANGE_REPLAY("https://plate-software.de/errors/exchange-replay", 409), ORG_VALIDATION_FAILED("https://plate-software.de/errors/org-validation-failed", 400), INVITATION_EXPIRED("https://plate-software.de/errors/invitation-expired", 410), INVITATION_REVOKED("https://plate-software.de/errors/invitation-revoked", 410); // ... each maps to a `type` URI + status code } - W4-3 Ensure all v0.1 custom exceptions map to
ProblemDetail:ExchangeHmacInvalidException→ 401 +EXCHANGE_HMAC_INVALIDtypeExchangeReplayException→ 409 +EXCHANGE_REPLAYtypeExchangeExpiredException→ 401 +EXCHANGE_EXPIREDtypeOrgValidationException→ 400 +ORG_VALIDATION_FAILEDtype- Generic
BadCredentials→ 401 +https://plate-software.de/errors/bad-credentials
- W4-4 Security: Problem Details responses must not leak stack traces, SQL fragments, or
internal class names. The
detailfield is consumer-safe; theinstancefield is the request path. Test T-SEC13. - W4-5 Set
Content-Type: application/problem+jsonon all error responses from plate-auth endpoints. v0.1's default Spring Boot error body is replaced.
Done when: A failed login returns application/problem+json with type, title, status,
detail, instance. A replayed exchange envelope returns 409 with the EXCHANGE_REPLAY type.
T-UT19 + T-IT12 validate.
6.2 Configurable invitation expiration
Steps:
- W4-6 Add to
PlateAuthProperties:@Data public static class Invitation { @Min(1) @Max(90) private int expirationDays = 7; } - W4-7 Update
InvitationService.create(...)— replace hardcodedDuration.ofDays(7)withDuration.ofDays(properties.getInvitation().getExpirationDays()). - W4-8 Bean-validate:
@Min(1) @Max(90)prevents 0/negative/absurd values. Fails fast at startup. - W4-9 Document the config in Architecture §3.3:
plate: auth: invitation: expiration-days: 7 # default; range 1–90
Done when: Setting plate.auth.invitation.expiration-days=3 produces invitations that expire
in 3 days. Setting it to 0 or 100 fails startup validation. T-UT20 validates.
7. W5 — TypeScript improvements + Edge-runtime safety
Goal: Two frontend DX improvements:
- TypeScript type exports + Zod schemas — export proper types for
Membership,Invitation,AccessRequestand add runtime-validation Zod schemas for the exchange envelope + DTOs. - Edge-runtime safe
useAccessToken()— replace the App-Router-onlygetSession()call with an Edge-compatible approach.
Branch: feature/sprint-1/w5-ts-edge
7.1 TypeScript type exports + Zod schemas
Steps:
- W5-1 Create
packages/auth/src/types/index.ts— export domain types mirroring backend entities:export interface Membership { id: string; userId: string; orgType: string; orgId: string; role: MembershipRole; status: MembershipStatus; } export type MembershipRole = 'OWNER' | 'ADMIN' | 'MEMBER' | 'VIEWER'; export type MembershipStatus = 'ACTIVE' | 'SUSPENDED' | 'REVOKED'; export interface Invitation { id: string; email: string; orgType: string; orgId: string; role: MembershipRole; status: InvitationStatus; expiresAt: string; // ISO-8601 } export type InvitationStatus = 'PENDING' | 'ACCEPTED' | 'REVOKED' | 'EXPIRED'; export interface AccessRequest { id: string; requesterId: string; orgType: string; orgId: string; requestedRole: MembershipRole; status: AccessRequestStatus; } export type AccessRequestStatus = 'PENDING' | 'APPROVED' | 'DENIED' | 'EXPIRED'; export interface TokenResponse { accessToken: string; refreshToken: string; user: PlateAuthUser; memberships: Membership[]; } export interface PlateAuthUser { id: string; email: string; role: 'USER' | 'ADMIN'; firstName?: string; lastName?: string; } - W5-2 Re-export from
packages/auth/src/index.tsso consumers canimport { Membership, Invitation, AccessRequest } from '@platesoft/auth'. - W5-3 Update
useMemberships()return type from implicitanytoMembership[]. UpdateuseAccessToken()return type to explicitstring | null. - W5-4 Add Zod schemas in
packages/auth/src/schemas/index.ts:import { z } from 'zod'; export const ExchangeEnvelopeSchema = z.object({ wireVersion: z.literal(1).or(z.literal(2)), provider: z.enum(['google', 'microsoft', 'email', 'password']), providerSubject: z.string(), email: z.string().email(), name: z.string().optional(), inviteToken: z.string().optional(), nonce: z.string().uuid(), iat: z.number().int().positive(), }); export const TokenResponseSchema = z.object({ accessToken: z.string(), refreshToken: z.string(), user: z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(['USER', 'ADMIN']), firstName: z.string().optional(), lastName: z.string().optional(), }), memberships: z.array(z.object({ orgType: z.string(), orgId: z.string().uuid(), role: z.enum(['OWNER', 'ADMIN', 'MEMBER', 'VIEWER']), status: z.enum(['ACTIVE', 'SUSPENDED', 'REVOKED']), })), }); - W5-5 Use
TokenResponseSchema.parse()in the exchange client to validate backend response at runtime — catches contract drift early. - W5-6 Add
zodas a dependency (not peer dep) inpackages/auth/package.json.
Done when: import { Membership, Invitation, ExchangeEnvelopeSchema } from '@platesoft/auth'
resolves with full type safety. The exchange client validates the backend response via Zod.
T-FE08 + T-FE09 validate.
7.2 Edge-runtime safe useAccessToken()
Steps:
- W5-7 The current
useAccessToken()callsgetSession()which uses Node-only APIs. This throws in the Edge runtime. Fix: detect runtime and use a cookie-based fallback:Or, if called in a client component (browser), useexport function useAccessToken(): string | null { // Edge-runtime safe: read from a cookie set by the proxy handler const cookieValue = getCookie('plate-auth-token'); return cookieValue ?? null; }useSession()as before. - W5-8 Document the two modes:
- Client component (browser):
useSession()— token in NextAuth JWT (encrypted cookie) - Edge middleware: read
plate-auth-tokencookie (set by proxy handler)
- Client component (browser):
- W5-9 Add an Edge-runtime test (
@edge-runtime/vmorvitestwith Edge env) that importsuseAccessToken()and verifies it does not throw (T-FE10).
Done when: useAccessToken() works in both browser and Edge runtime without throwing. No
Node-only API imported in the Edge entry point. T-FE10 validates.
8. W6 — Wire-version assessment + integration tests + publish v0.2.0
Goal: Assess whether the wire-version needs bumping, write the v0.2 integration tests, and
publish v0.2.0 to the Gitea Package Registry.
Branch: feature/sprint-1/w6-publish
8.1 Wire-version assessment
Steps:
- W6-1 Review whether any v0.2 change alters the exchange envelope contract:
- MS Entra ID:
provider="microsoft"— already in the v0.1providerunion. No change. - Email magic-link:
provider="email"— already in the v0.1providerunion. No change. - LoginEventSink: backend-only, no envelope impact. No change.
- RFC 7807: response-only, no request/envelope change. No change.
- TS types/Zod: frontend-only, no wire impact. No change.
- Config expiration: backend-only. No change.
- MS Entra ID:
- W6-2 Decision point: If all changes fit the existing
ExchangeEnvelopeshape, thenWIRE_VERSIONstays at 1. v0.2.0 is a pure additive release.- If a provider-specific field is added to the envelope, then bump
WIRE_VERSIONto 2. - Prediction: No bump needed. The envelope is provider-agnostic by design.
- If a provider-specific field is added to the envelope, then bump
Done when: Wire-version decision is documented in CHANGELOG.
8.2 Integration tests
Steps:
- W6-3 Implement T-IT10..13 (new integration tests for v0.2):
- T-IT10: MS Entra exchange flow — mock MS JWKS, verify token → user → JWT
- T-IT11: Invitation mailer IT —
plate.auth.mail.enabled=true+ GreenMail → email sent - T-IT12: RFC 7807 error responses — failed login returns
application/problem+json - T-IT13: Multi-provider IT — Google + MS + email all configured, each completes exchange
- W6-4 Run full regression:
mvn -pl plate-auth-starter verify+mvn -pl it verify+ all v0.1 tests still pass.
Done when: All v0.1 + v0.2 tests green against Testcontainers Postgres.
8.3 Publish v0.2.0
Steps:
- W6-5 Update
CHANGELOG.mdwith v0.2.0 release notes (new features + migration notes). - W6-6 Bump version:
pom.xmlrevision →0.2.0,packages/auth/package.json→0.2.0. - W6-7 Cut
v0.2.0git tag. Gitea Actions publishes both artifacts. - W6-8 Verify:
mvn dependency:get de.platesoft:plate-auth-starter:0.2.0+npm view @platesoft/auth@0.2.0from a fresh machine.
Done when: v0.2.0 tag publishes both artifacts to the Gitea Package Registry.
9. Security review checklist additions
Incremental over v0.1's security checklist (Sprint-0-Plan §9). Items marked
[v0.2-new] are new for this sprint.
9.1 MS Entra ID provider [v0.2-new]
- MS Entra
id_tokensignature verified against MS JWKS endpoint (not just decoded). - Tenant ID (
tid) extracted and stored inuser_identities.tenant_id. - Provider is
@ConditionalOnProperty. Default disabled. Fail-fast without client-id/secret. tenantIdconfig exposed:common,organizations, or a specific GUID.
9.2 Email magic-link [v0.2-new]
allowDangerousEmailAccountLinking = false— verified by snapshot test (T-FE07).- Magic-link request response is generic — no email enumeration (T-SEC12).
- Magic-link token TTL configurable (default 600s/10min). Token is single-use.
- Magic-link emails do not contain PII beyond the targeted email address.
9.3 LoginEventSink SPI [v0.2-new]
- SPI invocation is asynchronous — does not block login flow (T-UT16).
- Failing sink logs WARN but never propagates exception to the login path.
LoginEventpassed to sink does not contain the password or full JWT.
9.4 Real mailers [v0.2-new]
MailInvitationMailerfails loudly on SMTP failure (throws, does not swallow).- Invitation email does not leak the org's internal ID — uses
OrgDisplayNameResolver. - Accept URL uses HTTPS (validated by config).
plate.auth.mail.enabled=false(default) → logging fallback. No SMTP connection attempted.
9.5 RFC 7807 Problem Details [v0.2-new]
- Error responses include no stack traces, no SQL fragments, no internal class names (T-SEC13).
detailfield is consumer-safe (human-readable, no secrets).typeURIs are stable and documented.- Problem Details responses set
Content-Type: application/problem+json. - Login-failure Problem Detail says "invalid credentials" — no user-exists leak.
9.6 Wire-version [v0.2-new] (only if bumped)
- If
WIRE_VERSIONbumped to 2: backend rejects version-1 envelopes with a clear error. - If bumped: both consumers upgrade lockstep per versioning policy.
- If not bumped: no action needed — v0.2 is additive.
10. Rollout plan
10.1 Consumer upgrade path
v0.2.0 is additive (assuming no wire-version bump). Upgrade for each consumer:
InspectFlow:
- Change
pom.xml:plate-auth-starterversion0.1.0→0.2.0 - Change
package.json:@platesoft/authversion0.1.0→0.2.0 - Enable MS Entra:
plate.auth.providers.microsoft.enabled=true+ MS client-id/secret env vars - Enable mail:
plate.auth.mail.enabled=true+ SMTP config - Optionally: configure
plate.auth.invitation.expiration-days - Optionally: add a
LoginEventSinkbean to tee events to Loki/SIEM - Frontend: update types —
useMemberships()now returnsMembership[] - Run full E2E suite — must pass
Sparkboard:
- Same version bumps
- Enable email magic-link if needed:
plate.auth.providers.email-magic-link.enabled=true+ SMTP - Optionally: import Zod schemas for runtime validation
- Optionally: use the default
MagicLinkCallbackcomponent
10.2 Rollback strategy
v0.2.0 rollback is safe — no Flyway migration is added (prediction). If a consumer needs to revert to v0.1.0:
- Revert dependency versions in
pom.xml+package.json - Redeploy
- Database is unaffected (no schema change)
11. Acceptance criteria
| # | Criterion | How verified |
|---|---|---|
| B1 | MS Entra ID provider works end-to-end | T-IT10 + T-E2E07 |
| B2 | Email magic-link provider works end-to-end | T-FE07 + manual test |
| B3 | LoginEventSink SPI fires on login events |
T-UT16 |
| B4 | Real InvitationMailer sends email |
T-UT17 + T-IT11 |
| B5 | RFC 7807 Problem Details on /api/auth/* |
T-UT19 + T-IT12 |
| B6 | Configurable invitation expiration works | T-UT20 |
| B7 | TS types + Zod schemas exported | T-FE08 + T-FE09 |
| B8 | Edge-runtime safe useAccessToken() |
T-FE10 |
| B9 | Both artifacts published at 0.2.0 | mvn dependency:get + npm view |
| B10 | All v0.1 tests still pass (no regression) | Full mvn verify + pnpm test |
12. Items deferred to v0.3+
- Multi-replica nonce store (Redis/Postgres UPSERT)
- Refresh-token rotation table + family-tracking (T-SEC10 from v0.1)
- JWT secret rotation via JWK Set +
kidheader - Session sliding vs absolute expiration toggle
requireRoles(['ADMIN'])middleware helper- WebAuthn / passkey provider (if consumer demand emerges)
- Per-app branding on exchange endpoint
- Account lockout after N failed logins
- 2FA / TOTP
13. Cross-references
- Assessment:
Sprint-1-Assessment.md - Test plan:
Sprint-1-Testplan.md - v0.1 plan:
Sprint-0-Plan.md - Architecture:
Architecture.md - Roadmap:
Roadmap.md(v0.2 section) - Open questions:
Open-Questions.md
End of plan v1. Ready for Plan Reviewer.