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
| Piece | What it is |
|---|---|
| Password hashing | bcrypt (Spring Security BCryptPasswordEncoder). |
| Access token | Signed JWT, HS384, 15-minute lifetime. Claims: userId, orgId, role, email, platformAdmin. |
| Refresh token | Opaque random 32-byte token, base64url-encoded, SHA-256 hashed at rest, 30-day lifetime. Rotated on every use. |
| Theft detection | Reuse of a revoked refresh token revokes every refresh token for that user. |
| Refresh-token cleanup | Daily scheduled job hard-deletes rows whose expires_at has passed; the audit trail keeps the history. |
| Multi-tenant isolation | Every request carries orgId from the JWT; the backend filters every query by it. |
Token lifecycle
- Login or register returns
{ accessToken, refreshToken, expiresIn, userId, orgId, role, email }. - Every API request carries
Authorization: Bearer <accessToken>. - On 401 the web client calls
POST /api/auth/refreshwith 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. - On logout
POST /api/auth/logoutrevokes 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
| Role | Powers |
|---|---|
| OWNER | Full control of the organisation. Can mint and demote OWNERs and ADMINs. |
| ADMIN | Can manage non-privileged users (ENGINEER / SURVEYOR / VIEWER). Cannot create or modify OWNER or ADMIN accounts. |
| ENGINEER | Full operational access: surveys, layers, forms, dashboards. |
| SURVEYOR | Field-focused: submissions, mobile capture. |
| VIEWER | Read-only. |
| BUYER | Marketplace-only role (where the marketplace is enabled). |
| Platform admin | Stratumly-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
| Method | Route | Purpose |
|---|---|---|
POST | /api/auth/register | Create a new org + first user (role OWNER) → token pair. |
POST | /api/auth/login | Exchange credentials → token pair. |
POST | /api/auth/refresh | Rotate access + refresh token (single-flight on the web side). |
POST | /api/auth/logout | Revoke refresh token. |
GET | /api/auth/me | Thin self: userId, email, role, orgId straight from the JWT. |
Self profile + password (/api/me)
| Method | Route | Purpose |
|---|---|---|
GET | /api/me | Richer self: adds firstName, lastName, orgName. |
PATCH | /api/me | Update own first / last name + email. |
POST | /api/me/password | Rotate password. Body: { currentPassword, newPassword }. |
GET | /api/me/sessions | List every active refresh token for the calling user. |
DELETE | /api/me/sessions/{id} | Revoke one session. |
Org-scoped user admin (/api/org/users)
| Method | Route | Purpose | Min role |
|---|---|---|---|
GET | /api/org/users | List colleagues in caller's org. | Any member. |
POST | /api/org/users | Create 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-everywhere | Force-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:
- JWT contract with the frontend stays stable. Access tokens are signed JWTs with
userId,orgId,role,emailclaims. The frontend doesn't care how they were minted. - Multi-tenant isolation. Every request carries
orgId. The backend filters every query by it. Cross-org reads or writes are not possible by construction. - Stateless API. No session state on the server. Access tokens are self-contained; refresh tokens are DB-backed but the access token is stateless.
- Sovereignty. Nothing customer-identifying leaves your deployment without an explicit per-feature decision (e.g. an SMTP provider).