# API-owned authentication and legacy compatibility
Status: API-owned authentication is the retained web's only login path, 30 October 2026.
The [API runtime guide](../../../api/adaptors/authentication/README.md) records
configuration and qualification. The retained NextAuth session owner and its
signed-assertion bridge have been retired. The API owns login and sessions;
Themelio validates those sessions through its compatibility adaptor.
## Ownership and retained behaviour
| Boundary | Responsibility |
| --- | --- |
| Go API authentication edge | OIDC start/callback, verified AAI evidence, technical browser sessions, session validation, logout and scoped access decisions. |
| Beto Party Governance | Stable Party identity, authentication eligibility/generation, AAI attribute projection and verified provider bindings through published commands/queries. |
| Themelio legacy auth adaptor | Implement the existing server-side auth/session interface using API authority and adapt retained route shapes. It does not own new provider decisions or privileged credentials. |
| Retained onboarding coordinator | Resume durable signup-time GitLab account/token and personal namespace setup, and request Waldur association through owner-defined closed ports. It records progress and hands qualified proof to Party Governance. |
| Waldur integration process | Implement separate identity-association and external-invitation adaptors. It alone receives the Waldur staff credential and exposes a different scoped capability for each port. |
| Retained AUP implementation | Present, read and write existing GitLab-backed acceptance until Phase 4. Expose a separately authenticated policy-only read to the API. |
| Beto Project | Canonical Project/member authority and Project fulfilment through its provider-neutral port, including required provider-user prerequisites. |
Keycloak remains the identity provider. The API owns its client protocol and
delegates canonical identity decisions to Party Governance over typed RabbitMQ.
OIDC, cookies, token storage and retained user response shapes stay outside the domain
and application layers.
Keep the working GitLab identifiers, namespaces, repositories, permissions and
provider-specific user fields used by unmigrated features. The auth adaptor maps
a verified Party/provider association to the existing user shape. It must never
substitute PartyId for a numeric GitLab user ID or derive ownership from an email.
Provider tokens remain in restricted server-side compatibility storage; canonical
session state carries opaque references where needed.
This is a refinement of the original coexistence wording. Preserving an existing
writer does not assign ownership of new behaviour to the process that happens to
host it. The previous wording allowed Waldur account and invitation decisions to
land in Next.js and the general compatibility worker. The refined boundary keeps
those callers as coordinators or HTTP adaptors and places each provider operation
behind the Party-owned port it implements.
## One session authority across both endpoint families
Use one API-owned canonical browser session. The preferred deployment exposes
both route families under the same public origin so the browser presents its
HttpOnly, Secure, appropriately scoped cookie to either family. The edge routes
migrated paths to Go and retained paths to Next.js. Cookie scope, SameSite,
trusted origins and CSRF protection must be qualified in the actual deployment.
The legacy adaptor validates the canonical session server-to-server with the API
before exposing a user or provider credential. Its cached display attributes and
credential handle confer no independent authority. The API checks current Party
eligibility and authVersion; owner commands still check their business authority.
On unavailable authority, both route families return an explicit unavailable or
pending outcome. Party rejection prevents normal access through either family.
The retained web exposes only a scoped policy-acceptance read, resolving the
verified Party/GitLab association server-side. This read bypasses browser auth
middleware and never calls API session validation. Its service credentials
authorise policy lookup only. Missing, stale or unavailable evidence cannot
become an accepted result. Historical Party protocols and stored bytes remain
unchanged; the deployed retained web issues no legacy session attestations.
An accepted Party identity can receive a limited onboarding session before
provider setup or AUP acceptance completes. Its route allowlist covers onboarding
status, basic self identity, policy presentation/acceptance, explicit resume and
logout. Normal business endpoints require current Party eligibility and current
AUP; operations using GitLab additionally require their relevant provider readiness.
Onboarding realtime subscriptions are equally restricted. Browser delivery uses
authorised Centrifugo projections, initial reads and history-gap reconciliation.
The legacy `auth()` shape alone is insufficient to enforce this boundary. Audit
middleware, server actions, protected layouts, session/token helpers, realtime
authorisation and logout call sites. Retained business handlers may stay intact
where shared gates cover them. Explicit exceptions require small boundary edits,
including an AUP screen that can run before normal business access is available.
The current inbound `PRIVATE-TOKEN` pass-through needs a separate qualified policy
for provider-token clients; it must not bypass canonical browser/Project authority.
Retained state-changing GET routes also need an explicit CSRF-safe transition.
## First-time signup
This flow assumes fresh v3 evidence with an eligible exact issuer/subject and a
unique `voperson_id`. The [attribute inventory and recovery rules](../../../../contracts/authentication/README.md#identity-attribute-inventory-and-recovery)
define every accepted claim and its source.
Provider association is qualified during setup; conflicts require operator attention.
Orange cards denote facts; only named published contracts are integration events.
Blue denotes commands, yellow aggregates, purple policies, green read/status
models and red exceptional outcomes.
```mermaid
flowchart TD
U["Person starts signup"] --> L["Command: start login
Go API"]
L --> K["Keycloak authenticates
API validates ID token + UserInfo"]
K --> R["Command: ResolveOrRegisterParty"]
R --> P["Aggregate: PartyAuthentication
Beto Party Governance"]
P --> E["Integration event: PartyRegistered
one PartyId, once"]
P --> IA["Durable hand-off: identity attributes"]
IA --> IP["Projection: latest AAI attributes
independent revision"]
IP --> IC["Integration event:
PartyIdentityAttributesChanged
contact fields only"]
IP --> SW["Durable hand-off: qualified Site identity
explicit issuer-to-Site enrolment"]
SW --> SB["Command: record Site provider binding
separate PartyProviderBinding root"]
SB --> BE
SW -.-> SX["Site claim missing / conflict
Site pending or needs attention"]
P --> A["Policy: identity accepted
current eligibility and generation"]
P -.-> X["Rejected / recovery required
no normal access"]
A --> S["Technical fact: limited API session issued"]
S --> B["Command: ensure compatibility setup
durable, resumable hand-off"]
B --> G["Provider association ports
GitLab retained actor + isolated Waldur identity adaptor
independent progress"]
G -.-> Q["Status: provider pending / attention
resume with stable work identity"]
G --> V["Command: record verified provider binding
independent Party Governance root"]
V --> BE["Integration event:
PartyProviderBindingEstablished"]
G -->|"Required GitLab setup ready"| F["Read model: current AUP
retained policy implementation"]
F --> C["Command: accept AUP
retained acceptance writer"]
C --> H["Fact: legacy acceptance recorded
no canonical PolicyAccepted event yet"]
H --> N["Policy: current Party + AUP
and readiness required by the operation"]
S --> N
BE -->|"Verified association for this operation"| N
N --> O["Read model: normal access available"]
O --> API["Migrated endpoints
Go API → Beto"]
O --> LEG["Retained endpoints
legacy auth adaptor → existing GitLab handlers"]
E -.-> M["Source Control: durable event consumers
owner queries and activation pending"]
BE -.-> M
classDef command fill:#dcecff,stroke:#6b94bd,color:#15283d
classDef aggregate fill:#fff0b8,stroke:#b69b43,color:#3d3213
classDef event fill:#ffd7ad,stroke:#c58a4b,color:#432a12
classDef policy fill:#eadffc,stroke:#9b7db5,color:#30223c
classDef read fill:#dff1df,stroke:#79a779,color:#203620
classDef hotspot fill:#ffe0e0,stroke:#c65f5f,color:#512020
class L,R,B,V,C,SB command
class P aggregate
class E,S,BE,H,IC event
class A,N policy
class F,O,M,IP read
class X,Q,SX hotspot
```
Party registration commits its receipt, outcome and outgoing intents atomically.
The registration event and private attribute observation can be delivered
independently; `GetCurrentParty` reports attributes as pending until the current
projection is available. Accepted authentication evidence remains available to
the trusted onboarding boundary without treating a stale display projection as
identity proof. Attribute changes do not advance authVersion.
Persist onboarding work before reporting it as scheduled. Repeated login and
browser interruption resume existing work; they cannot depend solely on the
one-time `PartyRegistered` event. Account setup has a stable Party/provider-scope
identity. Per-session credentials have separate session/generation-bound work.
Token issuance after an uncertain result must be reconciled before retrying.
Track provider progress independently: a Waldur failure can leave its operations
pending while ready GitLab-backed features remain usable.
Successful provider work records qualified proof and a durable binding hand-off.
The binding has its own revision and commit; provider completion cannot write
PartyAuthentication or Project membership in the same transaction. Normal access
must also wait for the verified association needed to resolve the operation's
credentials and acceptance evidence. Provider failure leaves the Party intact.
Signup creates no Project or membership. Existing Waldur membership may resolve
after a verified binding arrives; otherwise the user needs the invitation or
membership workflow. Metallo receives canonical facts and uses scoped owner
queries or its own projections. Signup performs no direct Metallo provisioning.
## GitLab and Project fulfilment
Project defines `ProjectProviderPort` for the representation and access it needs.
Its GitLab adaptor can ensure required provider users as prerequisites, then
reconcile groups and permissions. ProjectFulfilment records desired/confirmed
revisions, durable work and redacted delivery status.
During coexistence, prerequisite-user operations delegate to the retained
account writer. Signup and Project fulfilment must not independently create or
modify the same provider account. Transfer an operation only after its replacement
passes qualification and its former writer is disabled. Preserve uncertain-effect
recovery and stale-grant fencing for each target.
Signup-time personal repositories, credentials and Waldur synchronisation stay
explicit compatibility operations until their respective consumers move.
New provider operations keep administrator credentials outside API/Beto,
retained Next.js and the general compatibility worker. Existing retained
operations keep their current credential until their own migration. The Waldur
integration process exposes separate identity
and invitation capabilities, with distinct caller credentials, through closed
typed interfaces and qualified network isolation.
An Account Provisioning context is outside this slice. Reconsider a separate
context only when independent account business rules and consumers require it.
## Waldur association and invitation ports
Party Governance owns the binding invariant and the authenticated invitation
claim decision. Provider-specific mapping remains in driven adaptors:
```text
PartyProviderIdentityPort
ensureAssociation(verifiedIdentity, providerScope)
verifyAssociation(verifiedIdentity, externalAccountId)
ExternalInvitationPort
listForRecipient(verifiedRecipientEvidence, boundExternalAccountId)
accept(invitationId, verifiedRecipientEvidence, boundExternalAccountId)
```
Account existence is insufficient ownership proof. The Waldur identity adaptor
checks the enrolled issuer, configured identity source and existing account
provenance before calling Identity Bridge. A username or email match alone leaves
the work in `attention_required`. Retried effects keep the same work identity and
reconcile the exact external account before publishing binding proof.
The preflight cannot make Identity Bridge atomic. A concurrent account creation
after the provenance read can be mutated before the adaptor observes the lost
race and returns `identity_conflict`. Cutover therefore requires a qualified
provider-side condition or evidence that concurrent foreign creation is excluded.
Waldur remains the invitation store during coexistence. The API validates the
dedicated invitation operation, reads current verified contact evidence from
Party Governance and requires the established Party-to-Waldur binding. Each
accept command names one invitation. The adaptor records the provider outcome,
and Project observes accepted membership through its own Waldur source port.
```mermaid
flowchart LR
UI["Retained invitation UI"] --> API["Go API session authority"]
API --> PG["Party Governance
current contact + binding"]
PG --> IP["Party provider identity port"]
PG --> XP["External invitation port"]
subgraph WI["Isolated Waldur integration process"]
IA["Waldur identity adaptor"]
XA["Waldur invitation adaptor"]
WC["Shared Waldur transport"]
IA --> WC
XA --> WC
end
IP --> IA
XP --> XA
WC --> W["Waldur"]
W --> PS["Project-owned Waldur source adaptor"]
PS --> P["Project context"]
```
## Logout, cutover and qualifications
API session revocation immediately prevents both endpoint families from accepting
that session. Record durable compatibility credential cleanup and clear browser
cookies. Party disablement or a changed authVersion fences session authority
before asynchronous cleanup. Late callbacks, retries and token issuance results
must check the session generation/revocation state and cannot reopen it.
Direct provider credentials require their own revocation; show delivery failures
without treating them as permission to retain platform access.
Deliver this transition before migrating the selected business endpoints:
1. Specify technical OIDC/session, policy-only and compatibility-work contracts.
Preserve historical wire and stored-state compatibility during the cutover.
2. Implement API callback validation and limited sessions; replace the legacy
session dependency with the independent AUP port. Persist onboarding hand-offs.
3. Implement the legacy auth adaptor and audit every shared gate/caller. Reuse
retained provider operations behind resumable work with one writer per target.
4. Deploy API login/logout, retained authentication and the provider worker together.
Browsers with NextAuth sessions sign in again. NextAuth session authority and mode selection
are removed; installations require the API configuration before upgrading.
5. Migrate `/api/v1/me` and selected Project directory callers, then each further
endpoint after parity. Keep provider layout and retained business flows working.
6. In Phase 4, move AUP evidence/writes and the gate together to Party Governance.
Before switching, qualify exact issuer/audience, signature, nonce, state, PKCE,
expiry, auth_time, callback replay and session fixation. Exercise concurrent first
login, pending attributes, GitLab/Waldur outages, ownership conflict, crash after
external success, AUP acceptance before normal access, expired policy and outage,
cross-origin/CSRF checks, both endpoint families, two browser windows, disablement
and logout during token creation. Verify retained repository creation, Git HTTPS/LFS,
invitations, allocation views and deployment setup.
Keep browser delivery, API/Beto actor scopes, PostgreSQL atomicity, broker retry
and credential isolation in the relevant test lanes. Roll back only through a
qualified coordinated release rollback which fences issued sessions and preserves
Party identity and durable provider work; fallback on an individual auth failure
is never a cutover mechanism.