> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# API-owned login and compatibility contracts

Status: implemented behind explicit installation enrolment. The public browser
protocol is included in the assembled browser OpenAPI contract and generated web
client. The
[API runtime guide](/reference/services/api/adaptors/authentication/README.md) records the
route, worker and credential configuration. Retained Themelio requires API-owned
authentication; the NextAuth bridge has been retired. Limited sessions receive
only their own Party readiness notifications.
The <a href="/source/services/beto/docs/party-project/authentication-transition.md.txt">transition plan</a>
records the coordinated cutover and endpoint migration.

## Boundaries

| Specification                                                                                        | Server and caller                                                            | Responsibility                                                                             |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| <a href="/source/contracts/authentication/browser.openapi.yaml.txt">Browser protocol</a>             | Go API; browser                                                              | OIDC, limited session status, explicit onboarding resume and logout.                       |
| <a href="/source/contracts/authentication/legacy.openapi.yaml.txt">Legacy auth protocol</a>          | Go API; enrolled legacy adaptor or credential worker                         | Operation-scoped session validation, revocation and non-bearer credential-work fences.     |
| <a href="/source/contracts/authentication/compatibility.openapi.yaml.txt">Compatibility protocol</a> | Retained compatibility integration; enrolled API dispatcher or policy reader | Independent legacy AUP reads and durable account/credential work.                          |
| [Party authentication v3](/reference/contracts/party_governance/authentication/README.md)            | Beto Party Governance; enrolled API                                          | Canonical Party registration and current identity eligibility from qualified API evidence. |

Public technical paths live under `/auth`; private `/internal` paths are excluded
from public ingress. This interface adds no Beto business HTTP surface. Party
registration and queries still use typed RabbitMQ. These standalone specifications
remain separate from the business route registry. Private compatibility and
legacy specifications are absent from the public browser OpenAPI document.

Login start and RP-initiated logout accept native form POSTs followed by top-level redirects. Other
HTTP bodies are strict UTF-8 JSON; all request bodies have a 32 KiB limit. Reject duplicate keys,
unknown fields, explicit nulls, invalid canonical IDs and integers outside the
schemas' safe range. Responses use `Cache-Control: no-store`. TLS is mandatory
outside isolated tests. Logs, metrics, receipts and status projections exclude
cookies, session tokens, credentials, raw assertions and raw provider errors.
Provider IDs remain opaque values behind verified bindings.

Service credentials are distinct for legacy validation/revocation, credential
fence reads, accepted evidence, policy-only reads and compatibility dispatch. Their authentication
is independent of browser session middleware. Server enrolment restricts each
operation and provider scope; payload identifiers never establish caller identity.
Unavailable authority for an access decision returns `503 temporarily_unavailable`.
The basic session-status read may report dependent policy/provider unavailability
explicitly while Party/session authority is current; that view grants no access.

## OIDC and Party evidence

The API selects the enrolled issuer, client and scopes. An exact login `Origin`
selects one callback from the server's explicit allowlist. Browser input can only
request an allowlisted same-origin return path. Validate decoded paths, reject
protocol-relative URLs, backslashes, control characters and redirect destinations
outside that allowlist.

Start persists a random browser-bound login attempt, state, nonce, PKCE S256
verifier, selected callback, return path and expiry before redirecting. The token
exchange uses that same callback. A short-lived HttpOnly,
Secure, SameSite=Lax callback cookie supports the cross-site top-level callback;
it grants no platform authority. The normal `pharos_session` remains opaque,
HttpOnly, Secure and SameSite=Strict. Local HTTP development needs an explicit
isolated exception. Cookie names, scope and clearing must agree across both
endpoint families.

The callback requires the original browser binding and unconsumed state, exactly
one of `code` or `error`, and no duplicate parameters. Validate the exact enrolled
issuer, audience/authorised party, signature/key, nonce, expiry, authentication
age and PKCE exchange. If UserInfo is used, its subject must exactly match the
ID token. An error response is not authentication evidence. Provider error text
and tokens never enter redirect URLs. Callback responses use `no-store` and
`Referrer-Policy: no-referrer`. Success redirects to the token-free `/auth/complete`
landing document, which starts a fresh same-origin navigation to the configured
return path. This preserves Strict cookie delivery across an external IdP redirect
chain. Failure redirects to the fixed `/auth/error` retry page.

New logins use two sequential authorisations: online authentication establishes
browser SSO, then the complete enrolled scopes request offline consent. The
second callback uses fresh state, nonce and PKCE challenges saved under the same
attempt and expiry. Its verified issuer, subject, stable person ID and Site
username must match the first identity before Party registration or session
issuance. The callback cookie changes to the second state while retaining its
browser binding. An initial callback may redirect to the enrolled provider again;
only the completed consent callback sets `pharos_session` and returns through
`/auth/complete`. Provider consent policy remains effective, and online refresh
tokens are never sent to the offline-grant owner.

User-initiated sign-out revokes local authority and clears browser cookies before
redirecting through the issuer's discovered `end_session_endpoint`. The API sends
the exact registered browser-origin return URI and client identifier. It retains
no raw provider token for logout. The browser explains the provider confirmation
step. Operational failures redirect to `/auth/logout-error`, whose fixed HTML
page preserves cookies and offers an Origin-protected retry. The local-only `/auth/v1/logout` remains available
to the retained compatibility adaptor and other enrolled non-navigation callers.

Persist callback completion/checkpoints so a duplicate callback cannot exchange
a consumed code again, emit another registration or resurrect an expired/revoked
session. A definitive rejected or expired attempt requires fresh login. Uncertain
exchange/registration/session-write outcomes require recovery; browser retry
cannot invent new evidence under an existing command identity.

After validation the API signs the <a href="/source/contracts/authentication/assertion-v3.schema.json.txt">v3 assertion</a>.
Historical v2 assertions and accepted checkpoints retain their existing bytes.
Use the schema's protected header with an enrolled Ed25519 key. Beto accepts only
its enrolled API attester, key, exact OIDC issuer and v3 audience/type. `jti`
equals the registration command ID; `login_attempt_id` is API-generated and
correlated in request and result. `evidence_ref` is `sha256:` plus the lowercase
digest of the complete compact JWS. Request retries retain every signed byte,
command ID and correlation ID; transport request IDs may change.

Require `auth_time <= iat < exp`, lifetime and authentication age at most five
minutes, and at most 30 seconds of future clock skew. Reject duplicate, null,
unknown or ambiguous header/claim values before signature/schema acceptance.
Validate cryptography and enrolment as well as the JSON shape. An email must
carry `email_verified: true`; username/email alone never prove account ownership.
Issuer and subject remain exact strings. Claims confer no Project authority.

Keep the signing key in the API authentication boundary; Beto receives public
verification keys. Recheck current Party eligibility/generation before returning
a recorded registration outcome and before issuing a session. A fresh login
after disablement/re-enablement cannot reuse an older registration decision.

## Identity attribute inventory and recovery

| Claim                                                                                | Source and treatment                                                                                                                                                    |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss`, `sub`, `auth_time`                                                            | Validated ID token; issuer and subject remain exact authentication bindings.                                                                                            |
| `voperson_id`                                                                        | Required for v3; accepted from the validated ID token or matching UserInfo. Conflicting values reject login. Globally unique opaque identifier within Party Governance. |
| `site_username`                                                                      | Optional UserInfo claim only. A qualified, issuer-enrolled Site hand-off establishes a separate `site` provider binding. Absence does not reject platform login.        |
| `name`, `given_name`, `family_name`, `preferred_username`, `schac_home_organization` | UserInfo takes precedence when present; validated ID-token fallback when absent.                                                                                        |
| `email`, `email_verified`                                                            | Read together from one source; retain only the verified pair. A UserInfo email never borrows ID-token verification.                                                     |
| `voperson_external_affiliation`, `entitlements`                                      | Optional arrays of exact source strings, with absent and empty preserved. They confer no permissions or Project membership.                                             |

Recognised claims must have the declared types in both sources. Reject nulls,
control characters and coercion; unknown provider extensions are ignored.
Arrays contain at most 128 values of at most 2,048 bytes each. Individual limits
remain subject to the complete 16 KiB signed-assertion and 32 KiB work-request
limits. Required scopes are explicitly requested; access-token claims cannot
supply identity attributes.

GRNET currently describes Site usernames as `p[N][LLL][HHHH][Padding]`: the
first initial, three surname letters, a hash of the unique identifier and a
collision suffix when needed. For example, `pgfam7a3f` is an illustrative fixture
for Given Family; it does not specify the hash algorithm. Preserve the exact
AAI value for the downstream HPC POSIX username match. Validate type, length and
control characters without enforcing this provider naming pattern, deriving a
username, trimming or changing case. Native Site constraints belong to the Site
adaptor and its provider qualification.

An existing exact issuer/subject may adopt its first unique `voperson_id` without
changing PartyId. A changed subject for an existing `voperson_id` returns
`recovery_required`; it grants no session and creates no duplicate Party. Missing
or changed stable identity fails. Recovery requires an operator-authorised owner
command with an independently approved proof for that same stable identifier.
It preserves PartyId and access state, retires the former authentication binding
and advances `auth_version`, fencing previous sessions. Historical bindings
cannot register a new Party.

V3 issuer enrolment prevents fresh v1/v2 registration for that issuer. Historical
exact identities can use those protocols until adoption; an adopted root requires
v3. Default legacy installations retain their existing behaviour without v3
enrolment. This is a coordinated cutover requirement.

The canonical self query retains these source observations. Consumers needing
contact changes can subscribe to `PartyIdentityAttributesChanged`, which exposes
only contact fields and revisions. It carries no stable identity, authentication
subject, Site username, affiliation or entitlement claims. Site association is
reported through the existing provider-binding events.

## Sessions and legacy operations

The API stores technical session authority and exposes a non-bearer `session_ref`
plus `session_generation` only to scoped internal callers. These are distinct
from Party `auth_version`, root revisions and the browser bearer token. Session
expiry is fixed at issuance; status/validation calls do not extend it.
The runtime stores versioned v2 technical state separately and rewrites no v1
record. The runtime guide describes restart, fixed expiry and cutover behaviour.

An accepted Party may receive a limited onboarding session while provider setup
or AUP acceptance is pending. Every validation rechecks current Party eligibility,
authentication generation, local revocation and expiry. The legacy server selects
the registered operation from its route/action binding; browsers cannot choose it.

| Registered legacy operation       | Additional gate                                                                                                                                                 |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `legacy.onboarding.read`          | Basic self/status only; no provider credentials exposed to callers.                                                                                             |
| `legacy.policy.read`              | Verified GitLab association and required policy-read readiness; current AUP acceptance is unnecessary.                                                          |
| `legacy.policy.accept`            | Same prerequisites, plus Origin/CSRF protection at the browser boundary.                                                                                        |
| `legacy.onboarding.resume`        | Explicit current-session resume, with Origin/CSRF protection.                                                                                                   |
| `legacy.profile.read`             | Current AUP and required GitLab readiness.                                                                                                                      |
| `legacy.gitlab.request`           | Current AUP, verified binding and ready credentials; existing endpoint/provider permissions still apply.                                                        |
| `legacy.waldur.invitation.accept` | Current AUP, verified Waldur binding and ready Waldur association. The caller must use current Party contact evidence and a same-origin state-changing request. |

`200` from validation means only the returned operation is currently allowed.
The response must match the requested operation and current session fence.
`401` means missing/invalid/expired/revoked session, `403` means ineligible or
disallowed authority or required AUP, `409` means a pending provider/binding
prerequisite, and `503` means unavailable authority. Unknown operation IDs fail.
Never cache a positive decision as an independent grant.

Only after an allowed decision may the legacy adaptor map the canonical Party
through its verified binding to the existing GitLab user shape and retrieve the
server-side token from compatibility storage. Neither internal validation nor
browser status returns a provider token. A PartyId never replaces a GitLab ID.
Audit all shared helpers, middleware, layouts and server actions before switching.
Direct incoming `PRIVATE-TOKEN` clients require their own qualified policy and
cannot bypass these browser gates.

Policy-only lookup resolves the current established GitLab binding for the
requested Party. Unresolved association returns `409 binding_pending`; a retired
or foreign binding returns `403 forbidden`. It reads legacy acceptance without
calling either API session validation or NextAuth. `state: current` requires an
accepted version exactly equal to the current required version; otherwise state
is `required`. Provider/read failure returns `503`, and must not erase acceptance
or fabricate a policy version. The API retains this AUP gate on both route families.

The private policy path also supports `GET`, using the same enrolled reader
credentials and private-host checks. It accepts no query parameters or body and
returns only `authority`, `required_version` and `checked_at`. This requirement
read uses no Party, binding, acceptance or session lookup. SSH Access uses it
after confirmed binding absence; transport and authority failures still defer
revocation. The existing POST request and response shapes remain unchanged.

## Durable compatibility work

The retained integration owns each work record, command receipt, desired revision,
confirmed effects, recovery lease and outgoing hand-off. HTTP `202` acknowledges
their durable commit, never just an in-memory task. It returns the original
recorded command outcome on an exact replay; status reads return current progress.
No operation permits arbitrary GitLab/Waldur URLs, token scopes or role grants.

Account work has the business key `(PartyId, provider, instance, account_scope)`.
The server assigns its canonical `work_id`; concurrent requests for that key
converge on the same record and provider account. `expected_revision: 0` creates
absent work; later changes require its current revision. A command retry preserves
its ID, expected revision, generation, evidence and attributes. Changed material
under the same command ID is `409 idempotency_conflict`; stale new work is
`409 revision_conflict`. Fresh login uses the read-only account lookup by Party
and scope to resume the same work without creating another provider account.

The API keeps verified evidence privately recoverable until accepted work is
terminal, with its original validation provenance. The scoped evidence resolver
checks Party, authVersion, digest and enrolled provider scope; the worker compares
all returned correlations and attributes with its immutable command. Unavailable
or mismatched evidence cannot authorise an effect. An expired assertion cannot
authorise a new login; recorded ownership evidence is separate from current
Party eligibility. Recheck current Party authority before enabling effects. Identity conflicts require
attention; email matching cannot merge accounts. Attribute refresh and provider
progress leave authVersion unchanged.

Credential work uses a separate key `(session_ref, session_generation,
gitlab_binding_id)` and the complete immutable session fence. Before effects and
before exposing their result, the worker checks that fence through the API's
dedicated read. Resolve the binding as currently established, provider `gitlab`,
owned by the fence's Party and within the enrolled instance before issuance and
exposure. Unavailable authority pauses work. Tokens remain private to the
compatibility store; local retrieval stops at session expiry/revocation. The
retained provider's date-only expiry needs explicit revocation and reported lag.
Reconcile uncertain
issuance before retrying; an old result cannot confirm the newest desired revision.

Revocation immediately fences local API authority and durably records cleanup.
The compatibility revoke operation writes a terminal fence even when issuance
has not arrived; it does not require a current session or Party/AUP provider reads.
Late ensure requests/results cannot clear that fence. Cleanup succeeds only after
known and uncertain credentials have been reconciled/revoked. A provider outage
leaves cleanup pending while platform access remains revoked.

Each provider progresses independently. `ready` means all effects for the desired
revision are confirmed; `confirmed_revision <= desired_revision <= revision`.
Account readiness also requires the separately committed verified binding.
The private binding-reader lookup preserves the exact provider identity and
requires the returned binding to belong to the requested Party and enrolled
provider instance. Account work can recover an already established association
after rechecking current native ownership, evidence and canonical state. Retired
or foreign bindings remain blocked. This read changes no canonical authority;
the retained worker checkpoints the existing ID under its work lease and revision.
An account's proof is handed durably to Party Governance's existing private
binding operation; provider completion never writes a Party root transaction.
The [private binding ingress](/reference/contracts/party_governance/provider_binding/README.md)
verifies the retained writer's signed ownership fact and current Party identity.
Account work stays pending until its independently committed binding outcome
arrives. Its attester, provider scope and broker permissions require explicit
enrolment. The evidence resolver above supplies AAI provenance and does
not itself prove ownership of a GitLab/Waldur account.
No `PolicyAccepted` integration event is emitted for legacy acceptance.

Project fulfilment delegates overlapping user prerequisites to this same account
writer until a qualified per-operation transfer. Project groups/access remain
behind Project's port. This interface introduces no Account Provisioning context,
automatic Project membership or Metallo provider lookup.

## Delivery and compatibility checks

Browser status is an initial/reconciliation read; explicit resume is a user action.
These contracts enable no subscriptions or automatic browser refresh. Before an
interactive pending screen receives updates, declare a token-free typed Protobuf
projection and server-authorised Centrifugo subscription with revision/gap handling.
Browser polling is prohibited.

All declarations are additive. Existing `/auth/login`, `/auth/session`,
`/auth/logout`, `/auth/party/handoff`, private v1 Protobuf identities, signed
assertions, receipts and stored sessions keep their meanings. Activation needs
coordinated route/adaptor deployment and qualified old-session invalidation or
explicit import. No request failure triggers fallback to independent NextAuth login.

Run `pnpm generate:contracts`, `pnpm verify`, the schema/negative fixtures in
`scripts/test_authentication_contracts.py` and API authentication wire tests.
Schema fixtures test structure; protocol invariants, OIDC provider behaviour,
session fences, work atomicity and browser cutover have runtime tests and
separate environment qualification lanes.

<details class="page-source"><summary>Page source</summary>

<a href="/source/contracts/authentication/README.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
