Authentication
Nexa has three distinct identity surfaces. Each maps to one API surface and uses a different authentication mechanism. Mixing them is the most common integration mistake — read this page once before writing any auth code against the platform.
| Surface | Who | Mechanism | Token format | Lifetime |
|---|---|---|---|---|
| Operator | Airline ground staff, ops supervisors, finance auditors, Nexa admins | OIDC + Universal Login (Nexa-managed) | RS256 JWT | 15 min access / 30 d refresh w/ rotation |
| Passenger | Disrupted travelers, identified by PNR + last name (or magic link) | Signed magic-link / PNR-login session token | Short-lived JWT | 7 days, disruption-scoped |
| Partner (M2M) | B2B integrators (airline systems, corporate desks, regulators) | OAuth2 client credentials | RS256 JWT, distinct audience | 1 hour, no refresh |
Why three?
- Volume. Passengers number in the tens of thousands per disruption. A single shared identity model would create an operational dependency on the operator IdP during a disruption — the wrong coupling. Passenger auth must work even if the operator IdP is unavailable.
- Trust model. Operators are corporate users with airline IdP federation. Passengers are anonymous travelers identified by their booking. Partners are services with rotated secrets. Forcing one auth model on three trust models pushes complexity in the wrong direction.
- Token lifetime. Operator sessions span hours. Passenger sessions span the disruption (often under 24 h). Partner tokens live one hour. One unified TTL is wrong for at least two of them.
Operator authentication
Tenancy model
Each registered airline is a Nexa-managed organization. Internal Nexa staff have admin access across organizations. A user with no organization membership cannot log in — default-deny is enforced at issuance, so blank-tenant tokens are structurally impossible.
Token shape
Audience: https://api.nexa/v1.
{
"iss": "https://login.nexa.ai/",
"sub": "<opaque-user-id>",
"aud": "https://api.nexa/v1",
"exp": 1746489600,
"iat": 1746488700,
"scope": "openid profile email",
"https://nexa/airline": "urn:airline:latam",
"https://nexa/role": "OPS_SUPERVISOR",
"https://nexa/operator_urn": "urn:user:<opaque-user-id>"
}
The three https://nexa/... claims are added at login. Custom claims must be URL-namespaced; https://nexa/ is reserved for this purpose.
Deliberately not in the token:
- Permissions list (the role string is enough; inheritance is computed at runtime).
- Mutable per-tenant lookup data.
- PII beyond email.
Roles & inheritance
Each user is assigned exactly one role within their organization:
| Role | Inherits |
|---|---|
ADMIN | OPS_SUPERVISOR, AIRPORT_OPERATOR, FINANCE_AUDIT |
OPS_SUPERVISOR | AIRPORT_OPERATOR |
AIRPORT_OPERATOR | — |
FINANCE_AUDIT | — (parallel; read-only audit) |
Inheritance is computed by Nexa, not encoded in the token. The token carries the assigned role only.
Refresh-token rotation
Mandatory. Defaults:
- Rotation: enabled.
- Reuse detection: enabled — a single reuse event revokes the entire token family.
- Absolute lifetime: 30 days.
- Inactivity lifetime: 7 days.
The operator UI uses memory + httpOnly cookie storage. Local storage is forbidden (XSS risk).
On a detected reuse event, the next API call returns 401. The UI surfaces "Your session was ended for security; please sign in again." Don't try to silently recover — a reuse event means something compromised the token.
MFA
| Role | MFA |
|---|---|
ADMIN | Mandatory. WebAuthn or TOTP. SMS forbidden (SIM-swap risk). |
OPS_SUPERVISOR, FINANCE_AUDIT | Mandatory. WebAuthn or TOTP. SMS as last-resort fallback only. |
AIRPORT_OPERATOR | Org-configurable. Adaptive MFA defaults to step-up on risk signals (new device, impossible travel, high-risk IP). |
Logout
A correct operator logout sequence is:
- Close the live session to release any held entity locks immediately.
- Revoke the refresh token at the IdP.
- Clear local SPA state — in-memory tokens, IndexedDB caches, service-worker tokens.
A logout that completes only step 2 leaves locks held; step 1 must run before step 2 returns.
Emergency revocation
The 15-minute access-token TTL is the upper bound on natural revocation, which is too long for "this account is actively compromised, kill it now." Nexa exposes an emergency-revocation list that is checked on every authenticated request. Used for compromised accounts and forced session termination (HR offboarding mid-shift). Not for routine logout. Entries are removed manually after the underlying issue is resolved — there is intentionally no auto-expiry.
Passenger authentication (magic-link / PNR-login)
Passengers are not operator-IdP users. They are anonymous in the IAM sense, identified solely by their PNR + last name or a magic-link token the operator dispatches via SMS.
Two entry paths
Path A — magic-link via SMS: the platform sends an SMS link of the form https://<airline>.passengers.nexastudio.io?token=<jwt>. The token is a short-lived JWT carrying the case URN, group affinity, and an expiration. The mobile API validates the token at the edge.
Path B — PNR-login on the webapp: the passenger opens the PWA directly and enters their PNR (record locator) + last name. The PNR-login endpoint is aggressively rate-limited per-IP and per-PNR globally to defeat credential stuffing. Successful login mints the same session token shape as Path A.
Both flows yield a session token with claims:
{
"iss": "https://passengers.nexastudio.io/",
"sub": "urn:passenger-session:<uuid>",
"aud": "https://passengers.nexa/v1",
"exp": 1746489600,
"case_urn": "urn:case:c-7f8e1",
"group_id": "g-1124",
"tenant": "urn:airline:latam"
}
Lifetime & scope
- 7 days absolute lifetime (covers the disruption window).
- Disruption-scoped — a token for case A cannot read case B even within the same airline.
- Tokens are revoked when the case
CLOSEDtransition fires.
Why a separate signing surface?
Operator JWTs and passenger JWTs are signed by deliberately disjoint signing surfaces:
- A stolen operator token cannot impersonate a passenger (audience mismatch).
- A stolen passenger token cannot impersonate an operator (audience and signing-key mismatch).
- An incident at the operator IdP does not affect passenger auth.
Partner authentication (OAuth2 client credentials)
Partners receive an OAuth2 client ID + secret per registration. Each registration is per airline: a multi-tenant partner (e.g., a corporate travel platform) holds multiple registrations.
Token request
POST https://login.nexa.ai/oauth/token HTTP/1.1
Content-Type: application/json
{
"grant_type": "client_credentials",
"client_id": "<your-client-id>",
"client_secret": "<your-client-secret>",
"audience": "https://internal.nexa/v1"
}
Response:
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600
}
Token shape
Audience: https://internal.nexa/v1 — deliberately distinct from the operator audience. A stolen operator token replayed against a partner endpoint fails audience validation immediately.
{
"iss": "https://login.nexa.ai/",
"sub": "<m2m_client_id>@clients",
"aud": "https://internal.nexa/v1",
"exp": 1746492300,
"iat": 1746488700,
"scope": "cases.read cases.events.subscribe bookings.read",
"gty": "client-credentials",
"https://nexa/partner_urn": "urn:partner:corp-travel-x",
"https://nexa/tenant_urn": "urn:airline:latam"
}
Scopes
Initial scope set:
cases.readcases.events.subscribebookings.readvouchers.readdisruptions.events.subscribepolicies.readmetrics.readpassenger.notify(write — bounded per-partner)cases.priority.request(write — bounded per-partner)
Every endpoint declares its required scope; unauthorized requests fail with 403 Forbidden.
Token caching
Cache M2M tokens client-side until exp - 60s to avoid hammering the IdP. On a 401 from Nexa, force-refresh the token once before propagating the failure.
Secret rotation
Partner secrets rotate every 90 days. Nexa publishes a 14-day dual-key window during which both the old and new secrets are accepted. Rotate ahead of expiry; do not wait for the cutover day.
Tenant isolation, layered
For all three surfaces, tenant isolation is enforced in three reinforcing places:
- Token claim — the tenant identifier is stamped at issuance.
- Application-level guard — every query is filtered by tenant. CI asserts the guard is wired up at boot.
- Per-tenant data partitioning — operational data, event topics, and partner credentials are tenant-scoped end-to-end.
Cross-tenant reads are impossible, even for "platform-wide" partners. A regulator overseeing two airlines holds two registrations.
Common errors
| Status | Reason | What to do |
|---|---|---|
401 Unauthorized | Missing / invalid / expired token | Refresh the token (operator: refresh-token rotation; partner: re-request via client credentials). Do not retry blindly. |
403 Forbidden | Authenticated, but the role / scope / tenant mismatches | Check the required scope on the endpoint and the tenant claim. |
409 Conflict | Concurrent-write conflict or sub-case lock held by another operator | Reload the resource and retry; the response includes the current state. |
429 Too Many Requests | Rate limit exceeded | Honor Retry-After; idempotency-keyed retries within the same window are exempt. |
503 Service Unavailable | Downstream circuit open (partner outage) | Honor Retry-After. Do not auto-retry — that is what the circuit breaker exists to prevent. |
The WWW-Authenticate header on 401 carries the specific failure reason (invalid_token, expired_token, insufficient_scope).