HelloJohn / docs
Sessions

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:

TriggerMethod
User signs outPOST /v1/auth/sign-out
Admin revokesDELETE /v1/sessions/:id
Password changedAutomatic (optional)
Security incidentDELETE /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

StatusDescription
activeNormal active session
expiredPast session_duration; refresh token invalid
revokedManually terminated

On this page