Managing JWT Cookies, CORS, and Security in Angular Applications
When building modern Single‑Page Applications (SPAs) with Angular, developers often face the question of how to store and transmit JSON Web Tokens (JWTs) safely. Should the token live in a cookie, in local storage, or be sent in an Authorization: Bearer … header? How do CORS, HttpOnly, SameSite, and the Angular withCredentials flag interact? This article walks you through the security implications, best‑practice patterns, and practical code snippets so you can protect your Angular app from XSS and CSRF attacks while keeping authentication smooth.
1. Why JWTs Are Usually Sent in an Authorization Header
- Stateless authentication – The server validates the token on each request without needing a session store.
- Clear separation of concerns – Tokens are treated as credentials, not as UI data, so they belong in request headers.
- Built‑in support – Most back‑end frameworks (Express, Spring, .NET, etc.) provide middleware that reads
Authorization: Bearer <jwt>.
If you decide to keep the JWT in a cookie, you must understand the trade‑offs.
2. Storing JWTs in Cookies
2.1 HttpOnly vs. Accessible Cookies
| Attribute | Effect | When to use |
|---|---|---|
| HttpOnly | Browser does not expose the cookie to document.cookie or JavaScript. |
Ideal for preventing XSS theft of the token. |
| No HttpOnly | JavaScript can read the cookie, allowing you to copy it into an Authorization header. |
Needed only if the back‑end requires the token in a header and you cannot change that design. Not recommended because it opens XSS risk. |
Bottom line: If your API expects the JWT in a header, store the token outside of a cookie (e.g., in memory or a secure storage) or redesign the API to accept the cookie directly.
2.2 SameSite – The First Line of Defense Against CSRF
| SameSite value | Browser behavior | Recommended use |
|---|---|---|
| Strict | Cookie is sent only for same‑site navigation (no cross‑origin requests). | Best security, but may break legitimate third‑party embeds. |
| Lax (default in modern browsers) | Cookie is sent on top‑level GET navigation, but not on POST/PUT/DELETE cross‑origin XHR/fetch. | Good balance for most SPAs. |
| None | Cookie is sent on all cross‑origin requests if Secure is also set. |
Needed only for true cross‑origin APIs, but must be combined with CSRF tokens. |
Setting SameSite=Lax (or Strict) mitigates the classic Cross‑Site Request Forgery (CSRF) scenario where a malicious site forces the browser to submit a request that automatically includes the JWT cookie.
3. Angular’s withCredentials Flag
this.http.get('https://api.example.com/profile', { withCredentials: true })
- Purpose – Tells the browser to include cookies, Authorization headers, and TLS client certificates on a cross‑origin request.
- When it matters – If your Angular app is served from
app.example.comand the API lives onapi.example.com, the request is cross‑origin. WithoutwithCredentials: true, the browser won’t send the JWT cookie even if it exists.
Important notes
- Same‑origin requests (e.g.,
https://app.example.com/api) automatically include cookies;withCredentialsis unnecessary. - The HttpOnly flag does not affect whether the cookie is sent—only whether JavaScript can read it. The cookie will be attached to every request that matches its domain/path, regardless of
withCredentials. - CORS pre‑flight – The server must respond with
Access-Control-Allow-Credentials: trueand explicitly list allowed origins (wildcard*is not allowed when credentials are used).
4. Practical Implementation Scenarios
4.1 Preferred: JWT in HttpOnly Cookie, API Reads Cookie Directly
Server (Express example)
app.use(cookieParser());
app.get('/api/user', (req, res) => {
const token = req.cookies['jwt']; // HttpOnly cookie
if (!token) return res.sendStatus(401);
// verify token …
res.json({ name: 'Alice' });
});
Angular service
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
getProfile() {
return this.http.get<User>('/api/user', { withCredentials: true });
}
}
Cookie attributes: HttpOnly; Secure; SameSite=Lax; Path=/;
Result – The JWT never touches JavaScript, eliminating XSS exposure, while withCredentials ensures the cookie is sent on cross‑origin calls.
4.2 When the API Demands a Bearer Header (Legacy)
Work‑around (not recommended)
- Store the token in memory after login (e.g., a service property).
- Attach it to every request via an HTTP interceptor.
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = this.auth.getToken(); // in‑memory, never persisted
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Security tip: Because the token lives only in memory, a page reload clears it, forcing the user to re‑authenticate—this reduces the window for XSS theft.
5. Defending Against XSS & CSRF
5.1 XSS Mitigation
- Content Security Policy (CSP) – Restrict script sources (
script-src 'self'), block inline scripts ('unsafe-inline'), and enable nonce‑based scripts. - Framework auto‑escaping – Angular’s template binding (
{{ value }}) automatically HTML‑escapes output. AvoidinnerHTMLunless you sanitize first. - Sanitize user input – Use Angular’s
DomSanitizerfor any dynamic HTML.
5.2 CSRF Mitigation
- SameSite cookies (Lax or Strict) – Primary defense.
- Double‑submit cookie – Send a random token in a non‑HttpOnly cookie and echo it in a custom header (
X-CSRF-Token). - Anti‑CSRF middleware – Many back‑ends provide built‑in CSRF validation (e.g.,
csurffor Express).
6. Common Questions & Tips
| Question | Answer |
|---|---|
Do I need withCredentials for same‑origin calls? |
No. Browsers automatically include same‑origin cookies. |
Can I set SameSite=None without Secure? |
No. Modern browsers reject SameSite=None unless the cookie is also marked Secure. |
| What happens if a JWT cookie is HttpOnly and I try to read it in Angular? | You cannot read it via document.cookie. Instead, let the server read the cookie and return the needed data. |
| Is storing JWT in localStorage safe? | It is vulnerable to XSS because any script can read localStorage. Prefer HttpOnly cookies when possible. |
| How to test CORS with credentials? | Use browser dev tools → Network tab. Look for Access-Control-Allow-Credentials: true in the response and verify the request includes the Cookie header. |
Quick checklist before launch
- [ ] JWT stored in HttpOnly, Secure, SameSite=Lax cookie.
- [ ] API reads the token from the cookie (or redesign to accept header).
- [ ] Angular HTTP calls use
{ withCredentials: true }for cross‑origin APIs. - [ ] Server CORS config includes
Access-Control-Allow-Credentials: trueand a specificAccess-Control-Allow-Origin. - [ ] CSP header is enabled (
Content-Security-Policy: default-src 'self'; script-src 'self'). - [ ] Run an XSS scanner (e.g., OWASP ZAP) and a CSRF test suite.
7. Takeaway
Storing JWTs in HttpOnly, SameSite‑protected cookies and letting the server read them directly gives you the strongest defense against XSS while still supporting seamless authentication for Angular SPAs. Use Angular’s withCredentials flag only when you need to send those cookies across origins, and always pair it with proper CORS headers and a robust CSP. By following these patterns, you can build Angular applications that are both user‑friendly and resilient against the most common web‑security threats.