Febasidocs
Concepts

Sessions

Refresh tokens, rotation, atomic session limits, and the four reasons a session can end.

A session is the lifetime of a refresh token. Every successful /login opens one; every /logout closes one; rotations replace one without ever leaving a gap. Febasi Auth tracks every session it has ever opened and records why each one ended.

Anatomy of a refresh token

The refresh token returned to the client is a UUID — the primary key of the session row in the refresh_tokens table. The lookup on /refresh is keyed directly by this UUID. The row also captures:

FieldPurpose
idUUID. The value the client receives and sends back on /refresh.
user_idOwner of the session.
tenant_idTenant scope.
token_hashReserved 32-byte random value. Currently not used for verification.
expires_atDefault 7 days; configurable per tenant.
revoked_atTimestamp when the session was closed. NULL when active.
revoke_reasonOne of USER_LOGOUT, TOKEN_ROTATION, AUTOMATIC_SESSION_LIMIT, MANUAL_REVOKE.
ip_addressThe client's IP at session creation. Useful for audit.
user_agentThe client's user-agent at session creation.

Planned breaking change

A future release will split the refresh-token credential into an opaque identifier and a separate verifier hashed at rest, so that exposure of the database alone does not yield usable tokens. The wire format will change (longer, non-UUID string), and clients will need to store it verbatim without parsing. The change will be announced ahead of time with a migration window.

Session limits — why they're atomic

Each tenant configures a max-concurrent-sessions value. When a user logs in and is already at the limit, Febasi Auth performs the following inside a single transaction with SELECT FOR UPDATE:

Lock the user's active sessions

SELECT * FROM refresh_tokens
WHERE user_id = $1 AND revoked_at IS NULL AND expires_at > NOW()
FOR UPDATE

Count + decide

If the count is below the tenant limit, do nothing. If it's at or above, collect the oldest session(s) so the new one fits.

Revoke the oldest, mint the new

UPDATE refresh_tokens
SET revoked_at = NOW(), revoke_reason = 'AUTOMATIC_SESSION_LIMIT'
WHERE id IN (oldest_ids);

INSERT INTO refresh_tokens (...) VALUES (...);

This happens in the same transaction, so there is never a moment where the user has more than the configured limit of active sessions.

If the limit is configured strict enough that the user is rejected instead of being downgraded, Febasi Auth returns SESSION_LIMIT_EXCEEDED (HTTP 429) with the current and max counts in the payload.

Token rotation

Every call to /refresh rotates the token:

old refresh token  →  marked revoked (reason: TOKEN_ROTATION)
new refresh token  →  inserted, returned to client
new access token   →  generated from the freshest user state

The client must always replace its stored refresh token with the latest one. Reusing a revoked token is never accepted.

Why this matters

  • Stolen-token blast radius. A leaked refresh token is invalid the moment the legitimate client refreshes — token rotation cuts the attacker off.
  • Real-time logout. Revocation is immediate. The next refresh fails; outstanding access tokens still work until they expire (max 15 minutes by default), which is the cost of stateless JWTs.
  • Forensics. Every revoke_reason and revoked_at is preserved for audit.

Revoking sessions

Sessions can be closed in four ways. Each is recorded in the row's revoke_reason and emitted to the audit log.

ActionReason
POST /api/v1/logout with the refresh tokenUSER_LOGOUT
Successful /refresh (rotates the old one out)TOKEN_ROTATION
New login at the session limit (oldest evicted)AUTOMATIC_SESSION_LIMIT
Admin revocation through the token-management APIMANUAL_REVOKE

For admin revocation — listing active tokens, killing a single session, logging a user out of all devices, or invalidating every session in a tenant — see Session management.

Configuration knobs

These live under auth_config on the tenant.

PathDefaultWhat it controls
tokenConfig.accessTokenExpiration900Access-token lifetime in seconds.
tokenConfig.refreshTokenExpiration7Refresh-token lifetime in days.
sessionLimits.maxConcurrentSessions5Active sessions allowed per user.
sessionLimits.enforceLogouttrueAt the limit: true evicts the oldest; false rejects login with SESSION_LIMIT_EXCEEDED (HTTP 429).
sessionTimeout30Inactivity timeout in minutes.

Update via PATCH /api/v1/tenants/{id}/config. Existing sessions are not retroactively shortened — the new values apply to sessions created after the change.

On this page