Story AUTH-001: Identity module skeleton, platform schema, and Keycloak realm¶
Module: identity Slice: S0 (arch §11) — V0 envelope AUTH-001 (arch §0.1) Side: [BACKEND] Version target: V0 — v0.0.0 Priority: 1 Depends on: TENANT-S0 (provides the two Postgres datasources, Redis, and the docker-compose harness), AUDIT-S0 Can develop concurrently with: None — this is the Tier-0 story; it must merge before any other AUTH story Merge order: Before every other AUTH story Estimated complexity: M PRD User Stories: N/A - Technical Story (enables all later AUTH stories) Wireframe: N/A — backend only
Objective¶
Stand up the empty identity (AUTH) module so every later AUTH slice has a place to live. This story creates the Spring Modulith module, the six platform-database tables AUTH owns, the Keycloak papillon-platform realm (shared by all tenants in V0), and a JWT-parsing filter skeleton. Nothing here authenticates a real user yet — it is the foundation that AUTH-002 (login) and AUTH-005 (signed links) build on.
Backend Scope¶
Entities¶
Create the table skeletons for all six AUTH tables (full columns per arch §2.2). They are created now even though later stories fill in the behaviour, so migrations never have to be re-ordered:
- users — user_id PK, tenant_id (NULL only for PLATFORM_ADMIN), keycloak_user_id, username, display_name, email, mfa_phone, mfa_whatsapp_enabled BOOL DEFAULT false, role (ENUM: PLATFORM_ADMIN, CABINET_ADMIN, OPERATOR, SUPERVISOR, READ_ONLY), agency_scope UUID[], state (ENUM: PENDING_ACTIVATION, ACTIVE, LOCKED, INVITATION_EXPIRED, DEACTIVATED), tenant_frozen BOOL DEFAULT false, deactivated_at, deactivation_reason, password_failures_counter INT DEFAULT 0, version INT, created_at, created_by, updated_at.
- user_invitations — per arch §2.2 (token_hash, channel, expires_at = issued_at + 7 days, state).
- remember_device_cookies — per arch §2.2 (15-day expiry). Table exists in v0.0.0 but is unused until V1 (no MFA/remember-device in V0).
- mfa_bypass_codes — per arch §2.2. Table exists but unused until V1.
- signed_links — per arch §2.2 (token_hash UNIQUE, attempt_counter, cycle_counter, lockout_until, state, tenant_frozen, version, etc.).
- customer_auth_attempts — per arch §2.2 (append-only attempt log).
Migrations¶
All AUTH migrations live under db/migration/platform/ (arch §9). Version numbers must follow the highest existing TENANT migration. Create:
- V{N+0}__auth_users.sql
- V{N+1}__auth_user_invitations.sql
- V{N+2}__auth_remember_device_cookies.sql
- V{N+3}__auth_mfa_bypass_codes.sql
- V{N+4}__auth_signed_links.sql
- V{N+5}__auth_customer_auth_attempts.sql
- V{N+6}__auth_indexes.sql
Key DDL constraints (arch §9):
- users: CHECK (role = 'PLATFORM_ADMIN' OR tenant_id IS NOT NULL).
- users: CREATE UNIQUE INDEX idx_users_tenant_username ON users (tenant_id, username) WHERE tenant_id IS NOT NULL (partial unique — lets PLATFORM_ADMIN rows with tenant_id=NULL co-exist).
- signed_links: CREATE UNIQUE INDEX idx_signed_links_token_hash ON signed_links (token_hash).
- Partial index for active links per tenant (suspension cascade) per arch §2.3.
- All indexes from arch §2.3.
Service Layer¶
IdentityModule.java— Spring Modulith module marker in packagecom.altarys.papillon.pcs.controlplane.identity.JwtValidationFilter.java(skeleton) — extracts the Bearer token, validates the signature against the Keycloak JWKS (cached in-process), checksexpwith a 30 s clock-skew, and extracts the claim set. NO tenant logic, NO tenant-status guard yet (those land in AUTH-002).TenantStatusCache.java— interface plus a Redis-backed implementation stub (Key: auth:tenant-status:{tenantId},TTL 30 s). No callers yet.KeycloakAdminClientinterface (arch §10.1) defined now so later stories inject a mock:directGrant,createUser,assignRealmRole,setUserEnabled,logoutUser,setUserAttribute.
Keycloak realm config¶
infra/keycloak/papillon-platform-realm.json (arch §14):
- Realm: papillon-platform (single shared realm for all V0 tenants — arch §0.3, D-11).
- Realm roles: PLATFORM_ADMIN, CABINET_ADMIN, OPERATOR, SUPERVISOR, READ_ONLY.
- Confidential client papillon-backend (Direct Grant enabled). Public client papillon-frontend (PKCE, for V2 readiness).
- Access-token TTL 300 s; absolute session 28800 s (8 h).
- Custom claim mappers (from user attributes): tenant_id, agency_scope (multivalued), country_code, papillon_role.
- Local-dev test user [email protected] / Admin1234! / role PLATFORM_ADMIN.
- Add the Keycloak service to the shared docker-compose.yml (arch §14).
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| GET | /actuator/health |
— | 200 OK | none |
Validation Rules¶
- Migrations must apply cleanly on a fresh platform DB and be idempotent on re-run (Flyway).
- The
usersCHECK constraint must reject a non-PLATFORM_ADMIN row withtenant_id=NULL.
Multi-Tenant Considerations¶
All AUTH tables live on the platform datasource (platformJdbcClient), never on a tenant/BYO DB (arch §2.1, ADR-AUTH-01). This holds for both V1 DB profiles (shared DB and BYO-DB): auth data always stays under platform custody. tenant_id is a plain column on users/signed_links (no cross-DB FK; cross-DB references are validated at the application layer only, starting in AUTH-008).
Audit & Logging¶
No business events emitted by this story. Confirm the spring-modulith-events-jdbc outbox wiring is present so later stories can publish AUDIT events (arch §7.8).
Frontend Scope¶
N/A — backend only.
UI States (ALL REQUIRED)¶
N/A — backend only.
Acceptance Criteria¶
AC-001: Platform migrations apply cleanly
Given a fresh platform database at the latest TENANT migration version
When the AUTH Flyway migrations V{N+0}..V{N+6} run
Then all six AUTH tables and all indexes/constraints from arch §2.3 and §9 are created without error
And re-running Flyway is a no-op
AC-002: Keycloak realm boots with five roles and claim mappers
Given the papillon-platform realm JSON is imported on Keycloak (>= 26.6 in prod; 25.0 acceptable in Testcontainers)
When Keycloak starts
Then the realm exposes the five roles PLATFORM_ADMIN, CABINET_ADMIN, OPERATOR, SUPERVISOR, READ_ONLY
And the custom claim mappers tenant_id, agency_scope, country_code, papillon_role are present
And access-token TTL is 300 s and absolute session is 8 h
AC-003: Health check responds
Given the AUTH module is wired into the application
When GET /actuator/health is called
Then the response is 200 OK
AC-004: JWT from the test realm is parsed
Given a JWT issued by the test realm for [email protected]
When the JwtValidationFilter receives it
Then the signature is validated against the Keycloak JWKS
And the claim set (including papillon_role) is extracted without error
Compliance Rules¶
- No compliance obligation is triggered by skeleton/schema work.
- Reminder for downstream stories: CIMA Reg. 01-24 acceptability of the signed-link scheme is open (AUTH-OQ-01, AUTH-TV-01) and civil opposability of the magic link as proof of consent is
INCERTAIN - avocat requis(red flag #5). Neither blocks this story.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md(Spring Data JDBC, package conventions, Modulith).docs/standards/db-guidelines.md+docs/standards/multi-datasource.md(migration placement underdb/migration/platform/).docs/standards/multi-tenant-model.md(role enum, realm strategy).docs/standards/critical-rules.md(tenant isolation rule #1).
Testing Requirements¶
Unit Tests¶
usersCHECK constraint rejects non-PLATFORM_ADMIN with NULLtenant_id.- Partial unique index allows two PLATFORM_ADMIN rows with NULL
tenant_idbut rejects duplicate(tenant_id, username).
Integration Tests¶
- Testcontainers: Keycloak (
quay.io/keycloak/keycloak:25.0with the test realm JSON) + Postgres (platform) + Redis, reusing the TENANT S0 harness (arch §13.1). - Boot smoke: context loads,
/actuator/healthreturns 200. - A Direct-Grant token for
[email protected]is parsed by the filter and claims are extracted.
What QA Will Validate¶
- Migrations apply on a clean DB; realm imports with five roles + mappers; health endpoint green; a real Keycloak JWT is accepted by the filter skeleton.
Out of Scope¶
- Any login behaviour, MFA, remember-device, tenant-status guard logic (AUTH-002).
- Signed-link issuance/verification (AUTH-005).
- Tenant-lifecycle handlers (AUTH-003).
remember_device_cookiesandmfa_bypass_codesare created empty; MFA is V1, not V0.
Definition of Done¶
- [ ] All six migrations apply cleanly on the platform DB and are idempotent
- [ ] Keycloak realm imports with five roles + four claim mappers + correct TTLs
- [ ]
GET /actuator/healthreturns 200 - [ ] A test-realm JWT is parsed and claims extracted by JwtValidationFilter
- [ ] Unit + integration tests written and passing
- [ ] Tenant isolation respected (all AUTH tables on platform datasource; both V1 DB profiles unaffected)
- [ ] No silent failures on startup (fail fast if realm/DB unavailable)