Security Headers
Security headers returned by HelloJohn's API, and recommended headers for your own application when integrating HelloJohn.
Security Headers
Security headers protect browsers from common attacks like XSS, clickjacking, and MIME-type sniffing. This page covers the headers HelloJohn's API returns, and the headers you should configure on your own application.
Headers Returned by HelloJohn's API
HelloJohn sets the following security headers on all API responses:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cache-Control: no-storeHeader Explanations
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | Forces HTTPS for 2 years, including subdomains |
X-Content-Type-Options | nosniff | Prevents MIME-type sniffing attacks |
X-Frame-Options | DENY | Blocks the API from being embedded in frames |
Referrer-Policy | strict-origin-when-cross-origin | Limits referrer information to same-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Disables unnecessary browser features |
Cache-Control | no-store | Prevents caching of authentication responses |
Authentication responses (tokens, user data) additionally include:
Cache-Control: no-store, no-cache, must-revalidate, private
Pragma: no-cacheRecommended Headers for Your Application
Your frontend application and backend should also set security headers. The correct configuration depends on your framework.
Content Security Policy (CSP)
CSP is the most impactful header for preventing XSS. A minimal policy for an app using HelloJohn:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'nonce-{RANDOM_NONCE}';
img-src 'self' data: https:;
connect-src 'self' https://api.hellojohn.dev;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'Replace {RANDOM_NONCE} with a per-request random value. For inline scripts/styles, use nonces rather than 'unsafe-inline'.
If using HelloJohn's hosted UI (redirect flow), also allow:
connect-src 'self' https://api.hellojohn.dev;
frame-src https://auth.hellojohn.dev;HSTS
Strict-Transport-Security: max-age=31536000; includeSubDomainsStart without preload and add it only after you are confident you can maintain HTTPS indefinitely. Once submitted to the preload list, you cannot easily remove it.
Other Essential Headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()Framework Configuration Examples
Next.js
In next.config.ts:
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
];
export default {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};Express.js
Using Helmet:
import helmet from "helmet";
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", "https://api.hellojohn.dev"],
frameAncestors: ["'none'"],
},
},
hsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
})
);Nginx
server {
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Anti-clickjacking
add_header X-Frame-Options "DENY" always;
# MIME sniffing
add_header X-Content-Type-Options "nosniff" always;
# Referrer
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# CSP (customize to your app's needs)
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.hellojohn.dev; frame-ancestors 'none';" always;
}Caddy
your-app.com {
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.hellojohn.dev;"
-Server
}
}Cookie Security
When your backend sets cookies (e.g., for refresh tokens), use secure attributes:
res.cookie("hj_refresh_token", refreshToken, {
httpOnly: true, // Inaccessible to JavaScript
secure: true, // HTTPS only
sameSite: "lax", // Sent on top-level navigations, not cross-site requests
path: "/auth", // Restrict to auth routes
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
});Attribute Reference:
| Attribute | Recommended | Purpose |
|---|---|---|
HttpOnly | ✅ Required | Prevents JavaScript access (XSS mitigation) |
Secure | ✅ Required | HTTPS only |
SameSite=Lax | ✅ Default | CSRF protection for most use cases |
SameSite=Strict | Optional | Strongest CSRF protection; may break OAuth redirects |
Path | Recommended | Limit scope to auth routes |
Max-Age | Recommended | Set explicit expiry |
Never use SameSite=None without Secure. Avoid SameSite=None unless you need cross-site cookie delivery (e.g., embedded iframe authentication).
Testing Your Headers
Test your headers using these tools:
- securityheaders.com — Graded report for your URL
- observatory.mozilla.org — Mozilla Observatory
- curl:
curl -I https://your-app.com \
| grep -E "(strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy)"Self-Hosted HelloJohn Headers
If you're self-hosting behind a reverse proxy, ensure your proxy forwards headers correctly and does not strip HelloJohn's security headers:
# Nginx: pass headers from upstream
location /v1/ {
proxy_pass http://hellojohn:8080;
proxy_pass_header Strict-Transport-Security;
proxy_pass_header X-Content-Type-Options;
proxy_pass_header Cache-Control;
}Do not override Cache-Control or Pragma on auth endpoints — these prevent browsers from caching tokens.