Session Lifecycle
How HelloJohn sessions are created, maintained, refreshed, and terminated.
Session Lifecycle
Understanding the full lifecycle of a session helps you build secure, reliable authentication flows.
1. Session Creation
A session is created when a user successfully authenticates via:
- Email + password
- Magic link (after clicking the link)
- Social login (after OAuth callback)
- SSO (after SAML/OIDC callback)
- API (
POST /v1/auth/sign-in)
The response includes both tokens:
{
"access_token": "eyJhbGciOiJFZERTQSJ9...",
"refresh_token": "rt_01HABCDEF...",
"expires_in": 3600,
"session_id": "ses_01HABCDEF999888"
}2. Active Session
While the session is active, the client uses the access token to make API calls:
GET /api/profile
Authorization: Bearer eyJhbGciOiJFZERTQSJ9...The access token is a JWT you can verify locally. It expires after 1 hour (configurable).
3. Token Refresh
When the access token expires, use the refresh token to get a new one:
curl -X POST https://api.hellojohn.dev/v1/auth/token/refresh \
-H "X-Tenant-ID: tnt_01HABCDEF654321" \
-H "Content-Type: application/json" \
-d '{"refresh_token": "rt_01HABCDEF..."}'Response:
{
"access_token": "eyJhbGciOiJFZERTQSJ9...",
"refresh_token": "rt_01HABCDEF_new...",
"expires_in": 3600
}HelloJohn uses refresh token rotation: each refresh returns a new refresh token and invalidates the old one. This limits exposure if a refresh token is stolen.
Proactive Refresh
Refresh the access token before it expires to prevent interruptions:
class TokenManager {
private refreshTimer?: ReturnType<typeof setTimeout>;
setTokens(accessToken: string, refreshToken: string, expiresIn: number) {
// Refresh 60 seconds before expiry
const delay = (expiresIn - 60) * 1000;
clearTimeout(this.refreshTimer);
this.refreshTimer = setTimeout(() => this.refresh(refreshToken), delay);
}
async refresh(refreshToken: string) {
const res = await fetch("/v1/auth/token/refresh", {
method: "POST",
body: JSON.stringify({ refresh_token: refreshToken }),
});
const data = await res.json();
this.setTokens(data.access_token, data.refresh_token, data.expires_in);
}
}4. MFA Promotion
If MFA is enabled, sign-in returns a limited access token where mfa_verified: false. The session is promoted after MFA verification:
Sign-in → Token (mfa_verified: false)
→ Complete MFA challenge
→ Token (mfa_verified: true)Your backend should check mfa_verified in the JWT claims for sensitive operations.
5. Session Expiry
Sessions expire after the session_duration setting (default: 30 days). After expiry:
- The refresh token returns
token_expired - The user must sign in again
- The session status changes to
expired
{
"error": "token_expired",
"message": "Refresh token has expired. Please sign in again."
}6. Session Revocation
Sessions can be revoked before natural expiry:
| Trigger | Method |
|---|---|
| User signs out | POST /v1/auth/sign-out |
| Admin revokes | DELETE /v1/sessions/:id |
| Password changed | Automatic (optional) |
| Security incident | DELETE /v1/sessions?user_id=... |
After revocation, the refresh token is immediately invalid. The access token remains valid until its own expiry (up to 1 hour). Use short access token lifetimes (5–15 minutes) if immediate revocation is critical.
Immediate Token Invalidation
For scenarios requiring instant access token invalidation (financial apps, healthcare), configure a token introspection check in your backend:
// Check every request (higher latency, instant revocation)
const session = await hj.sessions.verify(token);
if (!session.valid) {
return res.status(401).json({ error: "session_revoked" });
}Session States
| Status | Description |
|---|---|
active | Normal active session |
expired | Past session_duration; refresh token invalid |
revoked | Manually terminated |