Refresh — httpOnly cookie
→ CSRF
httpOnly
Secure
SameSite=Strict
Path=/api/v1/auth
In implementation — the
__Secure- name prefix
(defense-in-depth: the browser accepts the cookie only with the
Secure flag).
Path —
/api/v1/auth, not just
/refresh: the cookie is needed for logout too — session
termination
uses it to identify the refresh token.
The checkbox controls the lifetime of the cookie, not the token:
-
Unchecked → session cookie (no Max-Age), removed when
the browser closes (shared machines).
-
Checked → persistent cookie with Max-Age = refresh
token TTL (30 d).
In both cases the refresh token in the DB lives by its own TTL.
Refresh token rotation
On every refresh — a new token, the old one is invalidated.
Grace period ~10 s: within this window a just-rotated token
is accepted again and returns the same new one —
a benign tab race is harmless. Presenting an old
token outside the window is reuse detection → revoke the entire
chain (family), not all of the user's sessions.
Access token in the SPA
Kept in JS memory (not localStorage) — protection against XSS.
Sent via Authorization: Bearer. On reload,
restored via /refresh.
Refresh queue — concurrent 401
A single shared /refresh for all requests that hit an
expired token. The interceptor holds the Promise of the current refresh
(or null):
- 401 while a refresh is already in flight → await the same Promise.
- 401 with no refresh in flight → start one, store the Promise.
- Success → all waiters retry their request with the new token.
- Failure → reject all, logout.
Safeguards:
-
Retry exactly once — a
_retry flag on the request.
A 401 on an already-retried request → immediate logout, no new
refresh round (loop protection).
-
Only a 401 from an expired access token triggers a refresh. A 401 from
/refresh itself → the refresh token is dead → logout without
recursion; a 401 on permissions passes through without a refresh.
-
A timeout on the refresh Promise — a hung request rejects all
waiters and leads to logout rather than holding them forever.
-
Across tabs, a single-use refresh token may be sent twice.
The rotation grace period
↑ Refresh token rotation absorbs the race: the late tab gets the same new token and
carries on.
Concurrent sessions
A session = one
token family
— a single sign-in from a device or browser; an active session is a
family with a live refresh token. The number of concurrent sessions is
unlimited; visibility and control depend on role:
-
User — own sessions, self-defense. Sees all
their sessions and can end any but the current one — one at a time or
“all others” (protection when compromised). The card carries the
device, IP, and last activity (from the
refresh token).
Screen —
session management.
-
Admin — others' sessions, without private details.
The owner's device, IP, and geo are not shown to the admin (private
data, not needed to respond): the
user card
shows only the active-session count and a blunt “end all”.
It deletes all refresh tokens — no new access tokens are issued, while
an already-issued stateless access JWT lives out its 15-minute
window (↑ JWT claims).
When the suspicious session is known but the owner is not, the
audit log
names the user (auth events carry the session id), and from there — the same card.
what v2 adds
Per-user session limit (default 5, configurable in
Admin Panel;
on overflow the oldest is ended automatically).
Sign-in alert from a new device / geo — an in-UI banner
plus an email.
Instant revocation of an access token via a jti blacklist in
Redis — without waiting out the 15-minute window.
Invalidation on deactivationv2
Revocation of refresh tokens + the access token's jti in a Redis
blacklist (TTL = access token lifetime).
-
Critical actions (password change, user management,
security settings, ending sessions) require re-entering
the password — even within an open session.
-
With MFA enabled, the check accepts a TOTP code.
-
After success — a grace period (a few minutes, tied to the
current session): adjacent critical actions pass without a
repeat prompt, then the check closes again.
-
An incorrect entry counts under the same rate limit as sign-in.
Standard: delete the refresh token from the DB (identified by
the refresh cookie) + clear the httpOnly cookie. The access token
expires on its own (15 min). The “Sign out” button —
admin header.
All devices: delete all of the user's refresh tokens
— by the user themselves, from the session-management UI.
Forced: the same “all devices”, but by an admin —
from the
user card.
All the user's refresh tokens are deleted; no new access tokens are
issued, and an already-issued stateless access JWT lives out its
≤15-minute window.