Skip to main content
Sign-in happens on the central login host, backed by the login service and its central refresh-token session. After hand-off, the regional shell persists the browser session and provides the refresh and organisation-switch bridges used by feature bundles. The Global API retains OTP, the legacy OIDC exchange, regional sign-out-everywhere, public signing keys, and the service-token mint.

Auth endpoints

Current central session routes

In deployed regions, /api/login/* is proxied on the serving origin. Local shell development uses the login API at http://localhost:3400.

Retained regional routes

Central and regional refresh tokens use separate tables and peppers and are not interchangeable. Use /api/login/auth/* only with a central login token, and /api/global/auth/* only with a retained regional token. The current shell uses the central routes; its regional callback remains only for a ceremony that was already in progress during the migration.

OTP verify response

A successful POST /api/global/auth/otp/verify returns a JSON object with the following fields:
The active organisation is auto-selected: if the user has one active membership it is chosen automatically; if there are multiple, the user’s home organisation is preferred, falling back to the earliest membership. Zero active memberships returns 403.

OIDC exchange

POST /api/global/auth/oidc/exchange redeems an OAuth2 authorisation code (PKCE flow) against the configured identity provider and issues a Stralos session. The code redemption happens server-side; the browser never holds an IdP token. This endpoint runs alongside POST /api/global/auth/otp/verify. Both credential paths produce an identical session DTO. No Stralos web client calls this endpoint. The shell retired its regional /auth/callback page, the only caller; sign-in goes through the central login host. Request body:
Success response: the same shape as POST /api/global/auth/otp/verify — accessToken, refreshToken, user, and memberships. The returned refresh token belongs to the regional session store. Do not send it to /api/login/auth/*. A client that needs the current central session flow must start again through the central login host. Error responses: User activation: if the authenticated user’s status is invited, the exchange automatically transitions it to active on first successful login. This is an identity fact — no additional action is required. Rate limiting: the exchange endpoint allows up to 10 attempts per IP address per minute. Unlike the OTP endpoints, it has no per-email limit because the request contains no email address.

Switching central sessions

POST /api/login/auth/switch-org re-points the central session to a different organisation without requiring the user to re-enter their credentials. It is authenticated with a refresh token. Request body:
Response: the same shape as POST /api/login/auth/refresh — a new accessToken, a rotated refreshToken, and the updated user object. The response does not include memberships. Error responses: A switch that races another refresh-token operation can receive 401 after the token has been consumed. Clients should treat this as a refresh failure and prompt the user to log in again.

Refreshing central sessions

POST /api/login/auth/refresh rotates a refresh token and re-mints an access token for the session’s server-held active organisation. The session’s active organisation is never changed by a refresh — use POST /api/login/auth/switch-org to change it. The request body accepts only refreshToken; an extra client-supplied organisationId is rejected with 400 without consuming the token. Error responses: The identity_session_cannot_refresh 403 is a distinct, typed outcome from a 401. The caller still holds a usable refresh token and can proceed to organisation selection.

Legacy regional sign-out everywhere

POST /api/global/auth/sessions/revoke-all records a request to invalidate all of the authenticated user’s retained regional sessions. It accepts only a refresh token from the Global API’s regional session store. Central login sessions use a separate table and pepper, so a central refresh token cannot authenticate to this route and receives 401. Request body:
Response: 202 {} — an empty JSON object. The endpoint writes no session state and mints nothing. The admin store applies the regional revocation asynchronously. Access tokens may continue to work until that state propagates, bounded by their configured lifetime of at most one hour. The caller’s own regional session expires along with the other regional sessions. This route covers only retained regional sessions. Central browser sessions use POST /api/login/auth/sessions/revoke-all, which relays a signed command to the admin API. A successful request returns 202 {}; a missing dependency or refused relay returns an error. Error responses:

Token types

Feature bundles attach the user access token to API calls:
Stralos uses two EdDSA (Ed25519) JWT types. User tokens are issued by the active credential or session service; service tokens are minted by stralos-api-global. Both carry typ, iss, iat, exp, and a kid in the JWS header. User tokens also carry email, org, and role (admin or member). Service tokens carry an organisation UUID or null, plus non-empty scope and aud arrays. A null-organisation service token must include the platform scope. User tokens must not carry scope. Service tokens must not carry role.

Token verification

Global, Estimate, Activities, Documents, Rates, Scheduler, and Assistant use EdDSA verification for protected routes. A token must carry alg: EdDSA, verify against the Global API JWKS, and satisfy the permitted strict claim schema. All except Scheduler accept user or authorised service tokens; Scheduler accepts user tokens only. Missing, malformed, forged, HS256, or expired credentials receive 401. These seven services have no shared-secret fallback or log-only authentication mode.

JWKS endpoint

The Global API publishes public signing keys at:
The endpoint is public and returns cache-control: public, max-age=300. Consumers cache the key set for up to five minutes. When a token references an unknown kid, the verifier performs one immediate refresh. A 30-second negative cache per unknown kid prevents repeated fetches for invalid identifiers. The current signer is the first configured key. Retained public keys remain available during rotation until the tokens they signed have expired.

Logout

POST /api/login/auth/logout deletes the central refresh-token session named by the presented token. It does not change other sessions or invalidate outstanding access tokens. Use POST /api/login/auth/sessions/revoke-all to request revocation of all central browser sessions. Request body:
Response: 200 {}. An unrecognised or already-removed token is treated as an idempotent no-op and also returns 200. Error responses:

Refresh tokens

Refresh tokens are bearer strings that are opaque to clients. The current implementation structures them as userId.sessionId.<256-bit secret>, stores only a hash in DynamoDB, rotates them on refresh, and uses a 30-day default TTL. Reuse detection revokes the session family. Sessions carry a model version. Sessions issued before the current model version are invalidated on their next refresh, requiring re-login. The session model is currently at version 3 (IDP-M7a); version 2 sessions are refused with stale on their next refresh.

Shared browser contract

The browser contract uses these localStorage keys:
  • stralos.auth.accessToken
  • stralos.auth.refreshToken
  • stralos.auth.user
The shell publishes a single-flight refresh bridge for feature bundles. See Frontend architecture.

Service enforcement

For Global, Estimate, Activities, Documents, Rates, Scheduler, and Assistant, each auth plugin applies to its non-exempt routes and rejects missing or invalid credentials.

OTP rate limiting

The OTP request and verify endpoints each enforce independent one-minute limits:
  • 10 requests per IP address;
  • 5 requests per email address.
Exceeding either limit returns 429 with a retry-after header. The counters are in-memory and per running service instance.

OTP request response

A successful POST /api/global/auth/otp/request returns a JSON object:
In a local development environment with OTP_EXPOSE_GENERATED=true, the response also includes the generated code:
The response shape is the same whether or not the email maps to a registered user; no information about account existence is returned.

Local OTP behaviour

In the standard local setup, SES email delivery is disabled. Set OTP_EXPOSE_GENERATED=true in .env (the default in .env.example) so that POST /api/global/auth/otp/request returns the generated OTP code directly in the response. Without exposure or a separately configured email transport, the local login flow cannot receive the code. Use generated-code exposure only in a local, isolated development environment. Any environment reachable from the internet — including shared dev, test, and production deployments — must not set this flag. An internet-accessible endpoint with OTP_EXPOSE_GENERATED=true returns the OTP code to any unauthenticated caller, reducing the login flow to zero-factor: knowing an email address is sufficient to obtain a valid session for an active user.