Modern web apps often use localStorage
for JWTs — but that’s risky.
localStorage
is accessible to JavaScript, so an XSS attack can easily steal your token.
The proper way: use HttpOnly cookies + CSRF tokens.
Here’s how to transform your existing /login
endpoint securely without breaking Kafka, Redis caching, or rate limiting.
🪜 Step-by-Step Migration Plan
Step 1: Switch from LocalStorage to HttpOnly Secure Cookie
-
Instead of returning the JWT in the response body, set it as an HttpOnly, Secure, SameSite=Strict (or Lax) cookie.
-
These cookies can’t be accessed by JavaScript — protecting against XSS.
-
No change is needed in your Kafka or Redis logic — they’ll continue working because you’re just changing how the token is delivered, not the backend authentication logic.
💡 Kafka login notifications and Redis login limiters will remain unaffected, since they trigger before token issuance.
Step 2: Introduce a CSRF Token
-
When a user logs in, generate a CSRF token (a random UUID).
-
Store this token:
-
Option 1: in Redis (recommended if you already use Redis)
-
Option 2: in an SQL table (
csrf_tokens
)
-
-
Send this token as a non-HttpOnly cookie or via a header (so frontend can read it).
-
Frontend includes this token in every state-changing request header:
X-CSRF-TOKEN: <token>
🛡️ The backend will reject any POST/PUT/DELETE without a valid CSRF token that matches the user’s session or Redis entry.
Step 3: Secure Cookie Configuration
Update your application.properties
:
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=Strict
If your frontend and backend are on different domains:
server.servlet.session.cookie.domain=.yourdomain.com
Step 4: CORS Configuration (Critical)
When using cookies for auth:
-
You must enable credentials and disable wildcards (
*
).
Step 5: Frontend Adjustments
-
❌ Remove
localStorage
usage for JWTs. -
✅ Use
fetch
oraxios
Store only the CSRF token in memory or
sessionStorage
.-
For POST/PUT/DELETE requests
Handle 403 (CSRF error) responses gracefully — show a message like “Session expired, please refresh or re-login.”
Step 6: Optional — Add Session Mapping (for Admin Panels or Token Revocation)
If you want to track or revoke tokens:
-
Add a
session_id
column in DB or Redis mapping:
session_id -> jwt_id
On logout or admin disable, revoke by session ID.
Step 7: Test OWASP Protections
Verify:
-
✅ No JWT in
localStorage
orsessionStorage
-
✅ Cookies have
HttpOnly
,Secure
, andSameSite
flags -
✅ CSRF token mismatch returns
403
-
✅ XSS payloads can’t read cookies
-
✅ Rate limiter still blocks excessive login attempts
-
✅ Kafka still receives login notifications
⚙️ Additional Considerations
🗄️ Database Changes
-
Optional: Add
csrf_tokens
table or store in Redis (csrf:{sessionId} → token
).
🔧 Config Updates
-
Add cookie + CORS settings to
application.properties
. -
Ensure backend sends cookies via
ResponseCookie.from()
in Spring.
💻 Frontend
-
Remove token storage logic.
-
Add
withCredentials: true
in requests. -
Always attach
X-CSRF-TOKEN
header on write requests.
✅ Summary
Protection | Mechanism | Mitigates |
---|---|---|
HttpOnly cookie | JWT in secure cookie | XSS |
CSRF token | Separate token validation | CSRF |
Input sanitization (already using Jsoup) | Clean username/password | Injection |
Rate limiting (already in place) | IP-based limiter | Brute force |
Kafka login events | Audit trail | Security monitoring |