HelloJohn / docs
Security

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-store

Header Explanations

HeaderValuePurpose
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadForces HTTPS for 2 years, including subdomains
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing attacks
X-Frame-OptionsDENYBlocks the API from being embedded in frames
Referrer-Policystrict-origin-when-cross-originLimits referrer information to same-origin
Permissions-Policycamera=(), microphone=(), geolocation=()Disables unnecessary browser features
Cache-Controlno-storePrevents caching of authentication responses

Authentication responses (tokens, user data) additionally include:

Cache-Control: no-store, no-cache, must-revalidate, private
Pragma: no-cache

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; includeSubDomains

Start 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
  }
}

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:

AttributeRecommendedPurpose
HttpOnly✅ RequiredPrevents JavaScript access (XSS mitigation)
Secure✅ RequiredHTTPS only
SameSite=Lax✅ DefaultCSRF protection for most use cases
SameSite=StrictOptionalStrongest CSRF protection; may break OAuth redirects
PathRecommendedLimit scope to auth routes
Max-AgeRecommendedSet 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:

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.


On this page