1
Sprint 1 Plan
Patrick Plate edited this page 2026-06-24 21:59:30 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:

  1. Scope + ground rules (this section)
  2. Workstream overview (6 workstreams, W1W6)
  3. Step-by-step per workstream (goal, steps, done-when)
  4. Security review checklist additions (incremental over v0.1's §9)
  5. 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)
  • W3LoginEventSink SPI + real InvitationMailer (JavaMailSender) + AccessRequestMailer admin 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.0 and de.platesoft:plate-auth-starter:0.2.0 ship together from the same v0.2.0 git tag.
  • Wire-version discipline. If the exchange envelope contract changes, bump WIRE_VERSION to 2. If it doesn't (providers fit existing shape), WIRE_VERSION stays at 1 and v0.2 is purely additive.
  • Branch policy: feature/sprint-1/<workstream> branches → PR → squash-merge to main.
  • 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 W1W5 ~1 day
Total ~6 days

W1W5 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:

  1. W1-1 Create service/MicrosoftEntraService.java (or extend OAuthService) to handle MS Entra ID token verification. Same pattern as Google:
    • Accept id_token from 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
  2. W1-2 Update LoginProvider enum — add MICROSOFT (if not already present from v0.1's email toggle). The enum is already provider-agnostic.
  3. W1-3 Add Microsoft provider config to PlateAuthProperties.Providers:
    @Data public static class Microsoft {
        private boolean enabled = false;
        private String tenantId = "common";  // "common", "organizations", or a specific GUID
    }
    
  4. W1-4 The exchange envelope already has a provider field. MS Entra payloads use provider="microsoft". No envelope schema change needed — the existing ExchangeEnvelope is provider-agnostic.
  5. W1-5 Register the provider conditionally:
    @Bean
    @ConditionalOnProperty(prefix = "plate.auth.providers.microsoft", name = "enabled", havingValue = "true")
    public MicrosoftEntraService microsoftEntraService(...) { ... }
    
    Fail-fast if enabled without 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:

  1. W1-6 Implement packages/auth/src/config/providers/microsoft.ts:
    export function microsoftProvider(opts: {
      clientId: string;
      clientSecret: string;
      tenantId?: string;  // default "common"
    }): NextAuthConfig.Provider;
    
    Uses NextAuth v5's MicrosoftEntraID provider.
  2. W1-7 Wire into createAuthConfig(opts) — add microsoft?: MicrosoftOpts to PlateAuthConfigOptions.providers. If present, add the MS provider to the provider list.
  3. W1-8 Update ExchangeEnvelope type — the provider union type becomes 'google' | 'microsoft' | 'email' | 'password' (already in v0.1's type, now exercised).
  4. 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

  1. W1-10 Verify against InspectFlow's seeded MS identities. The V5 migration (V5__add_microsoft_tenant_id_index.sql) already indexes user_identities.microsoft_tenant_id. A user with provider=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.


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:

  1. W2-1 The exchange envelope already supports provider="email". The backend ExchangeService.consume(...) path is provider-agnostic — it verifies the HMAC envelope and find-or-creates the user. No backend changes needed for the exchange itself.
  2. 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
    }
    
  3. 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:

  1. W2-4 Implement packages/auth/src/config/providers/email.ts:
    export function emailProvider(opts: {
      server?: SMTPConfig;  // or connection string
      from: string;
    }): NextAuthConfig.Provider;
    
    Uses NextAuth v5's Email provider (nodemailer-backed).
  2. W2-5 Wire into createAuthConfig(opts) — add email?: EmailOpts to PlateAuthConfigOptions.providers.
  3. 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.
  4. W2-7 Security: verify allowDangerousEmailAccountLinking=false is 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

  1. 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:

  1. LoginEventSink SPI — new extension point for consumers to tee login events to external systems (Loki, SIEM, webhooks) instead of (or in addition to) DB rows.
  2. Real InvitationMailer + AccessRequestMailer — replace v0.1's no-op loggers with JavaMailSender-backed implementations that actually send email.

Branch: feature/sprint-1/w3-spi-mailers

5.1 LoginEventSink SPI

Steps:

  1. 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);
    }
    
  2. 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.
        }
    }
    
  3. W3-3 Wire LoginEventSink into LoginEventService:
    • After loginEventRepository.save(event), call loginEventSink.emit(event) asynchronously (@Async or 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.
  4. W3-4 Register the SPI in PlateAuthAutoConfiguration @Import list.
  5. W3-5 Update Architecture §3.4 SPI table — add LoginEventSink as 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:

  1. 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);
        }
    }
    
  2. W3-7 The default InvitationMailer when plate.auth.mail.enabled=false (or unset) remains the existing LoggingInvitationMailer. The new MailInvitationMailer activates only when mail is explicitly enabled. This preserves v0.1's "boots green with zero config" guarantee.
  3. W3-8 Create email templates:
    • src/main/resources/templates/invitation.html — HTML body with accept-url placeholder
    • src/main/resources/templates/invitation.txt — plain-text fallback
    • Use Spring's MimeMessageHelper for multipart.
  4. W3-9 Fail-loudly: if plate.auth.mail.enabled=true but spring.mail.host is 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:

  1. W3-10 Create de.platesoft.auth.spi.defaults.MailAccessRequestMailer — same pattern as MailInvitationMailer. Activates with plate.auth.mail.enabled=true.
  2. W3-11 Two notification methods:
    • notifyAdmins(request) — sends to configured admin email list
    • notifyRequester(request) — sends approval/denial notification to the requester
  3. W3-12 Templates:
    • templates/access-request-admin.html — "New access request from {email}"
    • templates/access-request-decision.html — "Your access request was {approved/denied}"
  4. 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:

  1. RFC 7807 Problem Details — structured error responses (application/problem+json) on all /api/auth/* endpoints so consumers can build robust error UX.
  2. Configurable invitation expirationplate.auth.invitation.expiration-days (currently hardcoded 7d).

Branch: feature/sprint-1/w4-problem-details-config

6.1 RFC 7807 Problem Details

Steps:

  1. W4-1 Create config/PlateAuthProblemDetailHandler — extends Spring Boot 4's ProblemDetailExceptionHandler (Spring 6 / Boot 4 has built-in RFC 7807 support via @ControllerAdvice + ProblemDetail). Wire it as a @ControllerAdvice for the de.platesoft.auth.controller package.
  2. 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
    }
    
  3. W4-3 Ensure all v0.1 custom exceptions map to ProblemDetail:
    • ExchangeHmacInvalidException → 401 + EXCHANGE_HMAC_INVALID type
    • ExchangeReplayException → 409 + EXCHANGE_REPLAY type
    • ExchangeExpiredException → 401 + EXCHANGE_EXPIRED type
    • OrgValidationException → 400 + ORG_VALIDATION_FAILED type
    • Generic BadCredentials → 401 + https://plate-software.de/errors/bad-credentials
  4. W4-4 Security: Problem Details responses must not leak stack traces, SQL fragments, or internal class names. The detail field is consumer-safe; the instance field is the request path. Test T-SEC13.
  5. W4-5 Set Content-Type: application/problem+json on 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:

  1. W4-6 Add to PlateAuthProperties:
    @Data public static class Invitation {
        @Min(1) @Max(90)
        private int expirationDays = 7;
    }
    
  2. W4-7 Update InvitationService.create(...) — replace hardcoded Duration.ofDays(7) with Duration.ofDays(properties.getInvitation().getExpirationDays()).
  3. W4-8 Bean-validate: @Min(1) @Max(90) prevents 0/negative/absurd values. Fails fast at startup.
  4. W4-9 Document the config in Architecture §3.3:
    plate:
      auth:
        invitation:
          expiration-days: 7   # default; range 190
    

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:

  1. TypeScript type exports + Zod schemas — export proper types for Membership, Invitation, AccessRequest and add runtime-validation Zod schemas for the exchange envelope + DTOs.
  2. Edge-runtime safe useAccessToken() — replace the App-Router-only getSession() call with an Edge-compatible approach.

Branch: feature/sprint-1/w5-ts-edge

7.1 TypeScript type exports + Zod schemas

Steps:

  1. 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;
    }
    
  2. W5-2 Re-export from packages/auth/src/index.ts so consumers can import { Membership, Invitation, AccessRequest } from '@platesoft/auth'.
  3. W5-3 Update useMemberships() return type from implicit any to Membership[]. Update useAccessToken() return type to explicit string | null.
  4. 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']),
      })),
    });
    
  5. W5-5 Use TokenResponseSchema.parse() in the exchange client to validate backend response at runtime — catches contract drift early.
  6. W5-6 Add zod as a dependency (not peer dep) in packages/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:

  1. W5-7 The current useAccessToken() calls getSession() which uses Node-only APIs. This throws in the Edge runtime. Fix: detect runtime and use a cookie-based fallback:
    export 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;
    }
    
    Or, if called in a client component (browser), use useSession() as before.
  2. W5-8 Document the two modes:
    • Client component (browser): useSession() — token in NextAuth JWT (encrypted cookie)
    • Edge middleware: read plate-auth-token cookie (set by proxy handler)
  3. W5-9 Add an Edge-runtime test (@edge-runtime/vm or vitest with Edge env) that imports useAccessToken() 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:

  1. W6-1 Review whether any v0.2 change alters the exchange envelope contract:
    • MS Entra ID: provider="microsoft" — already in the v0.1 provider union. No change.
    • Email magic-link: provider="email" — already in the v0.1 provider union. 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.
  2. W6-2 Decision point: If all changes fit the existing ExchangeEnvelope shape, then WIRE_VERSION stays at 1. v0.2.0 is a pure additive release.
    • If a provider-specific field is added to the envelope, then bump WIRE_VERSION to 2.
    • Prediction: No bump needed. The envelope is provider-agnostic by design.

Done when: Wire-version decision is documented in CHANGELOG.

8.2 Integration tests

Steps:

  1. 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
  2. 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:

  1. W6-5 Update CHANGELOG.md with v0.2.0 release notes (new features + migration notes).
  2. W6-6 Bump version: pom.xml revision → 0.2.0, packages/auth/package.json0.2.0.
  3. W6-7 Cut v0.2.0 git tag. Gitea Actions publishes both artifacts.
  4. W6-8 Verify: mvn dependency:get de.platesoft:plate-auth-starter:0.2.0 + npm view @platesoft/auth@0.2.0 from 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_token signature verified against MS JWKS endpoint (not just decoded).
  • Tenant ID (tid) extracted and stored in user_identities.tenant_id.
  • Provider is @ConditionalOnProperty. Default disabled. Fail-fast without client-id/secret.
  • tenantId config exposed: common, organizations, or a specific GUID.
  • 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.
  • LoginEvent passed to sink does not contain the password or full JWT.

9.4 Real mailers [v0.2-new]

  • MailInvitationMailer fails 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).
  • detail field is consumer-safe (human-readable, no secrets).
  • type URIs 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_VERSION bumped 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:

  1. Change pom.xml: plate-auth-starter version 0.1.00.2.0
  2. Change package.json: @platesoft/auth version 0.1.00.2.0
  3. Enable MS Entra: plate.auth.providers.microsoft.enabled=true + MS client-id/secret env vars
  4. Enable mail: plate.auth.mail.enabled=true + SMTP config
  5. Optionally: configure plate.auth.invitation.expiration-days
  6. Optionally: add a LoginEventSink bean to tee events to Loki/SIEM
  7. Frontend: update types — useMemberships() now returns Membership[]
  8. Run full E2E suite — must pass

Sparkboard:

  1. Same version bumps
  2. Enable email magic-link if needed: plate.auth.providers.email-magic-link.enabled=true + SMTP
  3. Optionally: import Zod schemas for runtime validation
  4. Optionally: use the default MagicLinkCallback component

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:

  1. Revert dependency versions in pom.xml + package.json
  2. Redeploy
  3. 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 + kid header
  • 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


End of plan v1. Ready for Plan Reviewer.