Security Headers for Authentication — Complete Implementation Guide
In this tutorial, you will learn about Security Headers for Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
Security headers are HTTP response headers that instruct the browser to enforce security policies, directly protecting authentication tokens and session data from common web attacks including XSS, clickjacking, MIME sniffing, and protocol downgrade attacks.
What You'll Learn
By the end of this lesson, you will configure all critical security headers, understand how each header protects authentication, implement Content-Security-Policy for XSS prevention, and set secure cookie attributes for session tokens.
Why It Matters
Security headers provide browser-level protection that complements server-side security. A properly configured CSP prevents XSS attacks that could steal auth tokens. HSTS prevents protocol downgrade attacks. These headers are the first line of defense for authentication security.
Real-World Use
A user accesses a banking application over a coffee shop WiFi. Without HSTS, an attacker could downgrade the connection to HTTP and intercept the session cookie. With HSTS, the browser knows to only use HTTPS. If an XSS vulnerability exists, CSP prevents the attacker from exfiltrating the session cookie.
Security Headers Architecture
flowchart LR
subgraph "Browser"
A[CSP Blocks XSS]
B[HSTS Enforces HTTPS]
C[X-Frame-Options Blocks Clickjacking]
D[SameSite Protects CSRF]
E[HttpOnly Protects Cookie from JS]
end
subgraph "Server"
F[Set Headers on Every Response]
G[Set Cookie Attributes]
end
F --> A
F --> B
F --> C
G --> D
G --> E
style F fill:#f90,color:#fff
Security Headers Implementation (Express)
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
baseUri: ["'self'"],
reportUri: "/csp-report",
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: "deny" },
noSniff: true,
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
}));
app.use((req, res, next) => {
res.setHeader("Permissions-Policy",
"geolocation=(), microphone=(), camera=(), payment=()"
);
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
next();
});
app.get("/api/login", (req, res) => {
res.cookie("session", "token-value", {
httpOnly: true,
secure: true,
sameSite: "strict",
domain: "example.com",
path: "/",
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ message: "Cookie set with secure attributes" });
});
Manual Security Headers (Python FastAPI)
from fastapi import FastAPI, Response
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"font-src 'self'; "
"form-action 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
)
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = (
"geolocation=(), microphone=(), camera=()"
)
return response
app.add_middleware(SecurityHeadersMiddleware)
@app.get("/health")
async def health():
return {"status": "ok"}
Security Headers Reference
| Header | Purpose | Recommended Value |
|---|---|---|
| Content-Security-Policy | Prevents XSS and data injection | Restrictive policy by origin |
| Strict-Transport-Security | Enforces HTTPS connections | max-age=31536000; includeSubDomains |
| X-Frame-Options | Prevents clickjacking | DENY |
| X-Content-Type-Options | Prevents MIME sniffing | nosniff |
| Referrer-Policy | Controls referrer header | strict-origin-when-cross-origin |
| Permissions-Policy | Restricts browser APIs | geolocation=(), microphone=() |
| Cross-Origin-Opener-Policy | Isolates cross-origin Windows | same-origin |
| Cross-Origin-Embedder-Policy | Prevents loading untrusted resources | require-corp |
Cookie Attributes for Auth
def set_secure_cookie(response, name, value, max_age=86400):
response.set_cookie(
key=name,
value=value,
max_age=max_age,
secure=True, # HTTPS only
httponly=True, # JavaScript cannot access
samesite="strict", # CSRF protection
domain="example.com",
path="/",
)
# For refresh tokens
def set_refresh_token_cookie(response, token):
response.set_cookie(
key="refresh_token",
value=token,
max_age=604800, # 7 days
secure=True,
httponly=True,
samesite="strict",
path="/api/auth", # Only sent to auth endpoints
)
# Clear cookie on logout
def clear_auth_cookies(response):
response.delete_cookie("access_token", path="/")
response.delete_cookie("refresh_token", path="/api/auth")
Common Mistakes
- Setting CSP with unsafe-inline and unsafe-eval everywhere defeats CSP's XSS protection.
- Forgetting to includeSubDomains in HSTS leaves subdomains vulnerable to downgrade.
- Setting X-Frame-Options to SAMEORIGIN when DENY is appropriate for auth pages.
- Not setting the
pathattribute on cookies, allowing tokens to be sent to unintended endpoints. - Using overly permissive CSP report-uri that leaks sensitive data in violation reports.
- Not testing security headers with tools like securityheaders.com or Mozilla Observatory.
Practice Questions
- How does Content-Security-Policy prevent authentication token theft?
CSP restricts which sources scripts can load and where data can be sent. Even if an XSS vulnerability exists, CSP blocks the attacker's script from loading external resources or exfiltrating tokens to attacker-controlled servers.
- Why is HSTS important for authentication?
HSTS tells the browser to always use HTTPS for the domain. This prevents man-in-the-middle attacks that downgrade HTTPS to HTTP, which would expose session cookies and auth tokens in plain text.
- How do cookie attributes work together to protect auth tokens?
HttpOnly prevents JavaScript access (XSS protection). Secure ensures HTTPS-only transmission. SameSite prevents cross-origin cookie sending (CSRF protection). Domain and Path restrict the cookie's scope.
- Challenge: Configure a complete security headers suite for a production application with CSP that allows necessary third-party scripts (analytics, CDN), HSTS with preload, all cross-origin isolation headers, and secure cookie configuration for both access and refresh tokens.
FAQ
Mini Project: Security Header Auditor
Build a CLI tool that checks a website's security headers, scores them against best practices, and provides remediation recommendations.
import requests
import sys
class SecurityHeaderAuditor:
HEADERS = {
"strict-transport-security": {"weight": 20, "good": "max-age=31536000"},
"content-security-policy": {"weight": 25, "good": "default-src 'self'"},
"x-frame-options": {"weight": 10, "good": "DENY"},
"x-content-type-options": {"weight": 10, "good": "nosniff"},
"referrer-policy": {"weight": 10, "good": "strict-origin-when-cross-origin"},
"permissions-policy": {"weight": 10, "good": "geolocation=()"},
"x-xss-protection": {"weight": 5, "good": "1; mode=block"},
}
def audit(self, url):
try:
resp = requests.get(url, timeout=10, allow_redirects=True)
except requests.RequestException as e:
print(f"Cannot reach {url}: {e}")
return
score = 0
max_score = sum(h["weight"] for h in self.HEADERS.values())
print(f"Security Header Audit: {resp.url}")
print(f"Status: {resp.status_code}")
print("=" * 60)
for header, config in self.HEADERS.items():
value = resp.headers.get(header)
if value:
score += config["weight"]
print(f"[OK] {header}: {value[:60]}")
else:
print(f"[!!] {header}: MISSING (recommended: {config['good']})")
print(f"\nScore: {score}/{max_score} ({score * 100 // max_score}%)")
if score < max_score * 0.7:
print("Grade: F - Critical headers missing")
elif score < max_score * 0.9:
print("Grade: C - Improve header configuration")
else:
print("Grade: A - Good security posture")
if __name__ == "__main__":
auditor = SecurityHeaderAuditor()
auditor.audit(sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3000")
What's Next
Build the authentication project to tie together all authentication patterns into a complete system.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro