Skip to main content

Authentication and profiles

There are four principal profiles, and they never mix — not in the cookie, not in the JWT, not in validation.

principal_typeWhoLoginExtra claims
managementOrganization owner/admin/auditorEmail + password
userParticipantPhone + OTP (no password)
operator, operator_type=pdvPDVUsername + password (account provisioned by management)operator_account_id, event_id
operator, operator_type=cashierCashierUsername + password (account provisioned by management)operator_account_id, event_id

Cookies isolated per profile

Every login (except the ones that don't issue a cookie yet — see each profile section) sets three cookies with names of their own for that profile:

wallet_management_access_token wallet_management_refresh_token wallet_management_csrf_token
wallet_user_access_token wallet_user_refresh_token wallet_user_csrf_token
wallet_pdv_access_token wallet_pdv_refresh_token wallet_pdv_csrf_token
wallet_cashier_access_token wallet_cashier_refresh_token wallet_cashier_csrf_token

The first two in each trio are HttpOnly. The third one (CSRF) is not — it exists precisely so same-origin JavaScript can read its value and echo it back in the X-CSRF-Token header on state-changing requests (double-submit cookie pattern). A client using a Bearer token in the Authorization header can ignore all three cookies entirely.

wallet-edge keeps a function (authenticationProfileForPath) that maps the URL path to the expected profile — regardless of which cookie the browser sent. That's what stops a management token from working against GET /v1/auth/wallet/me, for example: the route requires the wallet profile, the token is management, the Edge rejects with 401 AUTHENTICATION_PROFILE_MISMATCH before even calling the origin API. The origin API itself validates again (it never trusts the Edge's check alone).

The shared /v1/me was retired

Until recently, GET/PATCH /v1/me served both management and the participant at the same time. That was split into two isolated endpoints — each with its own guard, accepting only the matching principal_type:

BeforeNow
GET/PATCH /v1/meGET/PATCH /v1/auth/management/me (management only)
— (didn't exist)GET/PATCH /v1/auth/wallet/me (participant only)

PDV and cashier do not have (and there's no plan to add) an equivalent .../me endpoint. That's a deliberate decision: those accounts don't have a "personal profile" in app.user_profiles — whatever the PDV/cashier app needs (identifier, type, event) is already in the JWT's own claims.