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 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 transition plan records the coordinated cutover and endpoint migration.
Boundaries
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 v3 assertion.
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
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.
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
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.
Page source
Source snapshot ยท Revision f928834c8.