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.
Refresh Tokens: HttpOnly Cookie
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:
-
SameSite cookie attribute — Use
SameSite=Lax(minimum) orSameSite=Strict:Lax— Sent on top-level navigations, blocks cross-site POSTStrict— Never sent cross-site
-
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:
- The attacker uses the stolen token → gets a new token
- The legitimate user tries to refresh → old token is invalid
- HelloJohn detects reuse of a revoked token
- 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-originChecklist
- Access tokens stored in memory, not
localStorage - Refresh tokens in
HttpOnly; Secure; SameSite=Laxcookie - Backend proxy for token refresh (not direct client-to-HelloJohn)
- Sessions revoked on password/email change
- Short
access_token_ttlfor sensitive apps (15m) -
mfa_verifiedchecked for privileged operations - CORS configured to allow only your domains
- Security headers set on all responses