Skip to main content

Authentication & sessions

Stratumly authenticates users with a standard email + password flow, issues short-lived JWT access tokens and long-lived refresh tokens, and gives every user self-service control over their active sessions. Enterprise SSO (OIDC, SAML) and MFA sit on the roadmap and slot into the same model.

Model at a glance

PieceWhat it is
Password hashingbcrypt (Spring Security BCryptPasswordEncoder).
Access tokenSigned JWT, HS384, 15-minute lifetime. Claims: userId, orgId, role, email, platformAdmin.
Refresh tokenOpaque random 32-byte token, base64url-encoded, SHA-256 hashed at rest, 30-day lifetime. Rotated on every use.
Theft detectionReuse of a revoked refresh token revokes every refresh token for that user.
Refresh-token cleanupDaily scheduled job hard-deletes rows whose expires_at has passed; the audit trail keeps the history.
Multi-tenant isolationEvery request carries orgId from the JWT; the backend filters every query by it.

Token lifecycle

  1. Login or register returns { accessToken, refreshToken, expiresIn, userId, orgId, role, email }.
  2. Every API request carries Authorization: Bearer <accessToken>.
  3. On 401 the web client calls POST /api/auth/refresh with the refresh token. Concurrent 401s share a single in-flight refresh via a mutex (single-flight) and then retry. The old refresh token is revoked; a new pair is issued.
  4. On logout POST /api/auth/logout revokes the refresh token. The access token expires naturally within 15 minutes.

Why these shapes

  • Short access tokens. Server-side revocation isn't free; 15 minutes is short enough that revocation latency is bounded without paying it on every request.
  • Opaque refresh tokens. Refresh tokens carry no claims; they're a database key the server hashes and looks up. That makes them revocable, rotatable, and theft-detectable.
  • Single-flight refresh. A burst of 401s after a tab wakes from sleep would otherwise hammer the refresh endpoint; the mutex collapses them into one.

Roles

RolePowers
OWNERFull control of the organisation. Can mint and demote OWNERs and ADMINs.
ADMINCan manage non-privileged users (ENGINEER / SURVEYOR / VIEWER). Cannot create or modify OWNER or ADMIN accounts.
ENGINEERFull operational access: surveys, layers, forms, dashboards.
SURVEYORField-focused: submissions, mobile capture.
VIEWERRead-only.
BUYERMarketplace-only role (where the marketplace is enabled).
Platform adminStratumly-internal staff with cross-org visibility for support and infra. Marked by a separate platformAdmin claim on the JWT, not a role. Standard customer accounts never carry it.

Last-active-OWNER guard

Demoting or deactivating the only active OWNER returns 409 CONFLICT with a clear message. Self-edit of role or active status is blocked regardless of the caller's role.

Endpoints

Auth

MethodRoutePurpose
POST/api/auth/registerCreate a new org + first user (role OWNER) → token pair.
POST/api/auth/loginExchange credentials → token pair.
POST/api/auth/refreshRotate access + refresh token (single-flight on the web side).
POST/api/auth/logoutRevoke refresh token.
GET/api/auth/meThin self: userId, email, role, orgId straight from the JWT.

Self profile + password (/api/me)

MethodRoutePurpose
GET/api/meRicher self: adds firstName, lastName, orgName.
PATCH/api/meUpdate own first / last name + email.
POST/api/me/passwordRotate password. Body: { currentPassword, newPassword }.
GET/api/me/sessionsList every active refresh token for the calling user.
DELETE/api/me/sessions/{id}Revoke one session.

Org-scoped user admin (/api/org/users)

MethodRoutePurposeMin role
GET/api/org/usersList colleagues in caller's org.Any member.
POST/api/org/usersCreate user with one-time temp password (or supplied) → { user, temporaryPassword }.ADMIN (cannot mint OWNER / ADMIN).
PATCH/api/org/users/{id}Update role + active flag.ADMIN (only on non-privileged users).
POST/api/org/users/{id}/sign-out-everywhereForce-revoke every session for the target user.ADMIN.

Active sessions UI

Every authenticated user can open Settings → Security and see every active refresh token (device, IP, last seen, issued at) and revoke any of them. The same panel powers Sign out everywhere for a user's own account.

For administrators, the team page exposes a Sign out everywhere action against any user in the organisation. A revoked refresh token cannot be used to mint a new access token; the user falls back to login on next 401.

Audit log

Every successful and failed auth event lands in an immutable audit_log table:

  • Login (success / failure with reason).
  • Logout.
  • Refresh-token rotation.
  • Refresh-token theft detection (a reuse event revokes the whole tree).
  • Password change.
  • Profile update.
  • User creation, role change, deactivation.

The audit log is append-only: the operations team has read-only access to it for support and forensic queries; nobody can edit or delete entries.

On our roadmap

  • Password reset and email verification. Token-based reset and verification flow over SMTP.
  • OIDC social login (Google, Microsoft). "Sign in with Google" button, JWT issued in the same shape as password login.
  • TOTP MFA. Opt-in two-step for users; first compliance asks light it up.
  • SAML 2.0. For enterprise customers whose corporate IdP is the system of record.
  • SCIM provisioning. Alongside SAML, for automated user provisioning from enterprise directories.

When MFA, OIDC, or SAML are added, the JWT contract with the frontend stays unchanged; only the issuance path is different.

Invariants

These do not change as new auth paths are added:

  1. JWT contract with the frontend stays stable. Access tokens are signed JWTs with userId, orgId, role, email claims. The frontend doesn't care how they were minted.
  2. Multi-tenant isolation. Every request carries orgId. The backend filters every query by it. Cross-org reads or writes are not possible by construction.
  3. Stateless API. No session state on the server. Access tokens are self-contained; refresh tokens are DB-backed but the access token is stateless.
  4. Sovereignty. Nothing customer-identifying leaves your deployment without an explicit per-feature decision (e.g. an SMTP provider).