Security headers are the browser's mechanism for enforcing policies that protect users from cross-site attacks. For authentication flows, several are particularly relevant: headers that prevent your login page from being framed (clickjacking), headers that prevent credential leakage via Referer, headers that ensure cookies are only sent over HTTPS, and Content Security Policy that limits what scripts can execute on your pages. Getting these right is not optional for auth-sensitive applications.
Strict-Transport-Security (HSTS)
HSTS tells browsers to only connect to your site over HTTPS for a specified period. Once a browser sees this header, it will refuse to make plain HTTP connections to your domain and will automatically upgrade any http:// requests to https://. This prevents SSL stripping attacks where an attacker intercepts a redirect from http:// to https:// and keeps the session on plain HTTP.
// HSTS header — send on all HTTPS responses
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// Express middleware
app.use((req, res, next) => {
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
next();
});
The preload directive indicates you want your domain included in browser HSTS preload lists — hardcoded lists of domains that browsers ship with. This protects users who have never visited your site before. Submit your domain at hstspreload.org after confirming your entire site and all subdomains work correctly over HTTPS. An incorrect HSTS configuration can lock users out of your site for up to a year.
Content-Security-Policy (CSP)
CSP is the most powerful but also the most complex security header. It tells the browser which sources are allowed to load content on your page. A restrictive CSP prevents XSS attacks from executing scripts, exfiltrating tokens, or modifying the DOM to insert fake login forms.
For an authentication page, the most important CSP directives are script-src (which scripts can run), frame-ancestors (who can embed your page in a frame), and form-action (where forms can submit). A basic auth-page CSP:
# CSP for a login page — restrictive, no inline scripts Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests;
The frame-ancestors 'none' directive prevents your login page from being embedded in any frame or iframe, blocking clickjacking attacks. The form-action 'self' prevents a compromised script from redirecting form submissions to an attacker-controlled server. The base-uri 'self' prevents base tag injection attacks that could redirect all relative URLs.
'unsafe-inline' for scripts negates most of its security value against XSS — inline scripts are how most XSS payloads execute. Use a nonce-based CSP (script-src 'nonce-{random}') where each page load generates a fresh random nonce that is included in both the CSP header and the nonce attribute of legitimate script tags. Injected scripts without the nonce are blocked.X-Frame-Options
X-Frame-Options is the older mechanism for preventing your pages from being framed. With a modern CSP including frame-ancestors, you do not strictly need it — but sending both ensures protection in older browsers that do not support CSP.
X-Frame-Options: DENY
Use DENY (never frame this page) for login, MFA, and authorization pages. Use SAMEORIGIN only if you genuinely need your own site to frame these pages, which is unusual for auth pages.
Referrer-Policy
The HTTP Referer header leaks the URL the user came from to the destination site. For authentication flows, this is a significant problem. If your OAuth authorization URL includes query parameters (the state parameter, which often contains a CSRF token), and the user clicks a link on the authorization page, the Referer header on that request includes the full authorization URL including your CSRF token.
Referrer-Policy: strict-origin-when-cross-origin // What each value means: // no-referrer: never send Referer // no-referrer-when-downgrade: send on same-protocol, not http->https // origin: send only the origin (no path) // origin-when-cross-origin: full URL same-origin, origin-only cross-origin // strict-origin-when-cross-origin: recommended default — full URL same-origin, // origin-only for same-protocol cross-origin, nothing for https->http // unsafe-url: always send full URL (never use this)
For your authentication endpoints specifically, consider no-referrer or origin — there is no legitimate reason for external sites to know the exact URL of your OAuth authorization endpoint or login page.
X-Content-Type-Options
This single-value header prevents browsers from MIME-type sniffing. Without it, a browser might execute a file as JavaScript even if it was served with a non-JavaScript content type, which can lead to content injection attacks. For auth endpoints that return JSON, this prevents a browser from interpreting the JSON as HTML or JavaScript.
X-Content-Type-Options: nosniff
Permissions-Policy
Permissions-Policy (formerly Feature-Policy) limits which browser features are available on your page. For an authentication page, you want to disable features that are not needed and could be abused — microphone, camera (except if you use biometric auth), geolocation, and payment handlers.
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()
Putting it together in middleware
// Express: security headers middleware for auth pages
function authSecurityHeaders(req, res, next) {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=()'
);
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// Generate a fresh nonce for this response
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data:`,
`connect-src 'self'`,
`frame-ancestors 'none'`,
`form-action 'self'`,
`base-uri 'self'`,
`upgrade-insecure-requests`
].join('; '));
next();
}
app.use('/auth', authSecurityHeaders);