HelloJohn / docs
Sessions

Session Security

Best practices for securing sessions in HelloJohn — token storage, revocation, and threat mitigation.

Session Security

Sessions are the primary security boundary in your application. Protecting them prevents unauthorized access even if credentials are compromised.


Token Storage

Where you store tokens is the most important security decision.

Access Tokens: In-Memory

Store access tokens in a JavaScript variable, never in localStorage or sessionStorage:

let accessToken: string | null = null;

// Set after sign-in
function setAccessToken(token: string) {
  accessToken = token;
}

// Use in requests
async function apiFetch(url: string) {
  return fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
}

localStorage is accessible to any JavaScript on the page. A single XSS vulnerability can steal all stored tokens.

Store refresh tokens in an HttpOnly, Secure cookie set by your backend:

// Backend (Express/Node.js)
app.post("/auth/sign-in", async (req, res) => {
  const { access_token, refresh_token, expires_in } = await hj.auth.signIn(req.body);

  // Set refresh token in HttpOnly cookie
  res.cookie("hj_refresh_token", refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days in ms
    path: "/auth/refresh",
  });

  // Return only the access token to the client
  res.json({ access_token, expires_in });
});

The httpOnly flag prevents JavaScript from reading the cookie — it can only be sent by the browser.


CSRF Protection

If you store refresh tokens in cookies, protect against CSRF attacks:

  1. SameSite cookie attribute — Use SameSite=Lax (minimum) or SameSite=Strict:

    • Lax — Sent on top-level navigations, blocks cross-site POST
    • Strict — Never sent cross-site
  2. Double-submit cookie pattern — For older browser support:

// Include a CSRF token in the request header that matches the cookie
app.post("/auth/refresh", csrfProtection, async (req, res) => {
  const refreshToken = req.cookies.hj_refresh_token;
  const newTokens = await hj.auth.refreshToken(refreshToken);
  // ...
});

Revoking Sessions After Sensitive Actions

Revoke all other sessions when the user changes their password or email:

app.post("/user/change-password", async (req, res) => {
  const { userId, currentSessionId } = req.user;

  // Update password
  await hj.users.updatePassword(userId, req.body.newPassword);

  // Revoke all sessions except the current one
  await hj.sessions.revokeAll(userId, { except: currentSessionId });

  res.json({ success: true });
});

Detecting Suspicious Activity

Monitor sessions for anomalies using the session's ip_address and country fields:

async function detectAnomalies(userId: string, currentIp: string) {
  const sessions = await hj.sessions.list({ user_id: userId });

  const unusualSessions = sessions.filter((s) => {
    // Flag sessions from different continents
    return s.ip_address !== currentIp && isDistantLocation(s.country);
  });

  if (unusualSessions.length > 0) {
    // Notify user, require MFA re-verification, or revoke
    await hj.users.sendSecurityAlert(userId);
  }
}

Short-Lived Access Tokens for Sensitive Operations

For high-security endpoints, verify mfa_verified and optionally require MFA re-verification:

function requireMFA(req: Request, res: Response, next: NextFunction) {
  const { mfa_verified, iat } = req.token;

  // Require MFA verified within the last 15 minutes
  const mfaAge = Date.now() / 1000 - (req.token.mfa_verified_at ?? 0);

  if (!mfa_verified || mfaAge > 900) {
    return res.status(403).json({
      error: "mfa_required",
      message: "Please complete MFA to continue.",
    });
  }

  next();
}

app.post("/account/delete", requireMFA, handleDeleteAccount);

Refresh Token Theft Mitigation

HelloJohn uses refresh token rotation — each use invalidates the old token. If a stolen token is used:

  1. The attacker uses the stolen token → gets a new token
  2. The legitimate user tries to refresh → old token is invalid
  3. HelloJohn detects reuse of a revoked token
  4. HelloJohn revokes the entire session (configurable behavior)

This automatic reuse detection is enabled by default.


Security Headers

Set these headers on your application to harden session security:

Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

Checklist

  • Access tokens stored in memory, not localStorage
  • Refresh tokens in HttpOnly; Secure; SameSite=Lax cookie
  • Backend proxy for token refresh (not direct client-to-HelloJohn)
  • Sessions revoked on password/email change
  • Short access_token_ttl for sensitive apps (15m)
  • mfa_verified checked for privileged operations
  • CORS configured to allow only your domains
  • Security headers set on all responses

On this page