JWT access token is stateless and short-lived.
Default parameters from config:
alg=EdDSA(Ed25519 signature)kid=ed25519-v1typ=access+jwtiss=auth.tripidiumaud=api.tripidiumttl=5m
Access token claims:
sub=user_idsid=auth_session.idiss= configured issueraud= configured audienceexp=iat + AccessTokenTTLiat= issue time in UTCjti= random UUID of this exact token
Generation approach:
- Build
AccessTokenClaimswithsidplus standard JWT registered claims. - Sign the token with the configured Ed25519 private key.
- Set JWT header
kidandtyp.
Refresh token is opaque and stateful.
Default parameters from config:
- raw entropy length =
32bytes - session TTL =
7d
Generation approach:
- Generate
RefreshTokenLenrandom bytes usingcrypto/rand. - Encode raw bytes with base64url without padding.
- Compute SHA-256 from the encoded token.
- Store only the hash in the database.
- Return the raw refresh token to the client once.
Auth login flow:
- Parse form data from the request.
- Read
username/email, andpassword. - Validate provided
usernameand/oremail, and validatepassword. - Load the user by
usernameoremail. - Verify password against
user.password_hash. - Generate refresh token pair: raw token for cookie and SHA-256 hash for persistence.
- Parse client metadata:
created_ipfromr.RemoteAddr(host part ifhost:port)created_user_agentfromr.UserAgent()when present
- Generate identifiers:
session_id= new UUIDfamily_id= new UUID
- Compute
expires_at = now_utc + AccessSessionTTL. - Create
auth_sessionwith:id = session_iduser_idfamily_idrefresh_token_hashcreated_ipcreated_user_agentexpires_at
- Issue access JWT for
user_idandsession_id. - Set refresh token cookie with configured:
namepathdomainexpiresHttpOnlySecureSameSite
- Return JSON response with:
access_token
Current implementation note:
- refresh token is delivered via cookie, not via JSON body
- login response body contains
access_tokenonly
Get user flow:
- Parse
Authorizationheader withBearertoken. - Reject empty or malformed
Authorizationheader. - Parse JWT and allow only the expected signing algorithm
EdDSA. - Use
kidfrom JWT header to select a public key only from the trusted local key set. - Verify JWT signature with the selected public key.
- Validate claims:
typ == access+jwtiss == auth.tripidiumaud == api.tripidiumexp > nowsubis not emptysidis not empty
- Return
401 Unauthorizedwhen JWT is invalid. - Extract
subasuser_id. - Fetch user from the database by
user_id. - Verify that the user exists and
is_active = true. - Return
401or403for missing or inactive user according to the chosen policy. - Return user data.
Implementation split:
- Middleware validates JWT without database access.
- Middleware stores
user_id,session_id, andjtiin request context. GET /userhandler performs one database query to load the user.
Why these claim checks are required:
typprotects against accepting a token of another purpose.issprotects against accepting a token from another issuer.audprotects against accepting a token minted for another service.
Refresh must be based on refresh token only, not on access JWT, because the access token may already be expired at refresh time.
Preferred transport:
- read refresh token from
HttpOnlycookie - use
Securein production - set explicit
SameSitepolicy
Auth refresh flow:
- Read refresh token from request, preferably from
HttpOnlycookie. - Do not require access token for this endpoint.
- Validate refresh token format:
- token is not empty
- token has expected length
- token is valid base64url if that is the chosen encoding
- Compute
refresh_token_hash = SHA-256(raw_refresh_token). - Open database transaction.
- Find
auth_sessionbyrefresh_token_hashand lock the row withFOR UPDATE. - Return
401 Unauthorizedif session is not found. - Check whether the session is already revoked.
- If
revoked_at IS NOT NULL, treat the token as invalid:- this may indicate refresh token reuse after rotation
- revoke the whole token family by
family_id - return
401 Unauthorized
- If
expires_at <= now(), return401 Unauthorized. - Optionally mark expired session as revoked with reason
expired. - Load user by
user_id. - Verify that the user exists and
is_active = true. - Return
401or403according to the selected policy if the user is missing or inactive. - Generate a new refresh token pair:
- new raw refresh token for cookie
- new refresh token hash
- Generate new
session_id. - Keep the previous
family_id. - Mark the old session as rotated:
revoked_at = nowrevoke_reason = 'rotated'replaced_by = new_session_id
- Create new
auth_session:
id = new_session_iduser_id = old.user_idfamily_id = old.family_idrefresh_token_hash = new_hashcreated_ip = current IPcreated_user_agent = current User-Agentexpires_at = now + refresh/session TTLcreated_at = now
- Issue new access JWT:
sub = user_idsid = new_session_id- new
jti - new
iatandexp
- Set refreshed cookie with configured:
namepathdomainexpiresHttpOnlySecureSameSite
- Commit transaction.
- Return JSON response with:
access_token
Refresh rotation rules:
- every successful refresh must issue a new refresh token
- previous refresh token must be invalidated
- token lineage must be preserved through
family_idandreplaced_by - refreshed raw token should be returned via cookie, not via JSON body
Reuse detection:
- if a presented refresh token is found but already revoked, and it belongs to a rotated chain, treat the whole
family_idas compromised - revoke all active sessions in the same family
- return
401 Unauthorized - require full login again
Minimal status policy:
401 Unauthorizedfor token not found, expired, revoked, or reuse detected403 Forbiddenfor inactive user, if the API chooses to distinguish this case- alternatively inactive user can also be mapped to
401to avoid leaking details
Logout should do both:
- revoke the current refresh-backed session on the server
- clear refresh cookie on the client
Why both are required:
- deleting only the cookie is not enough, because the server-side session could still remain usable
- revoking only the database row is not enough, because the browser would still keep sending the stale cookie
Auth logout flow:
- Use
POST /auth/logoutwith access-token middleware. - Validate current access JWT using the same access-token checks as
GET /user. - Read current claims from request context:
subsid
- Use
sidas the identifier of the currentauth_session. - Revoke this session in the database:
revoked_at = nowrevoke_reason = 'logout'
- Make logout idempotent:
- if the session is already revoked, still return success
- Clear refresh cookie with
Set-Cookie. - Use the same cookie attributes that were used during login/refresh:
NamePathDomainSameSiteSecureHttpOnly
- Remove cookie with
Max-Age=0or anExpiresvalue in the past. - Return
204 No Content. - Client should discard access token from memory after successful logout.
Access token behavior after logout:
- access JWT is stateless, so an already issued access token may remain valid until
exp - this is acceptable when access-token TTL is short
- immediate access-token invalidation would require checking session state by
sidon every request
This endpoint returns active sessions of the currently authenticated user only.
Recommended flow:
- Use
GET /auth/sessionswith access-token middleware. - Validate access JWT using the standard access-token checks.
- Read claims from request context:
subasuser_idsidas current session ID
- Do not accept arbitrary
user_idfrom request parameters. - Query database only for sessions of the authenticated user.
- Return only sessions that are still active:
revoked_at IS NULLexpires_at > now()
- Mark the current session with
is_current = (session.id == sid). - Return metadata-only response.
Safe response fields:
idis_currentcreated_atexpires_atuser_agentin normalized formip
Fields that must not be returned:
refresh_token_hash- raw refresh token
family_idreplaced_by- revoke reasons and internal service fields
- unnecessary security metadata