Authentication and profiles
There are four principal profiles, and they never mix — not in the cookie, not in the JWT, not in validation.
principal_type | Who | Login | Extra claims |
|---|---|---|---|
management | Organization owner/admin/auditor | Email + password | — |
user | Participant | Phone + OTP (no password) | — |
operator, operator_type=pdv | PDV | Username + password (account provisioned by management) | operator_account_id, event_id |
operator, operator_type=cashier | Cashier | Username + 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.
Why the route decides the expected profile, not the cookie that arrived
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:
| Before | Now |
|---|---|
GET/PATCH /v1/me | GET/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.