Angular provides a built-in support for sending requests secured with the XSRF-TOKEN header. However, it won’t add the token to absolute URLs for security reasons. One way to solve the “Invalid CSRF token found” issue is to use relative links in all mutable requests and apply a custom proxy.
Debugging missing CSRF token
First, we have to be sure that Angular really doesn’t send requests with the X-XSRF-TOKEN. In the following section I’ll show you how to debug a Spring Boot application.
Adjust debug level on security logs
In order to see more detailed logs and enable security debugging add the following line to your application.properties
file:
1 2 3 |
# src/main/resources/application-development.properties … logging.level.org.springframework.security=DEBUG |
As a result, after restarting the app, you’ll be able to read messages logged by the CsrfFilter
class:
1 2 |
o.s.security.web.FilterChainProxy : /api/login at position 5 of 14 in additional filter chain; firing Filter: 'CsrfFilter' o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/login |
Add a breakpoint
Now you know exactly where to set a breakpoint. In other words, add it in the CsrfFilter::doFilterInternal
method (package: org.springframework.security.web.csrf
) and verify that the actualToken
is indeed null
:
Fix the invalid CSRF token error
When inspecting the request in the developer console you’ll see the 403 error on mutable requests because the Headers
section is missing the X-XSRF-TOKEN:
Avoid absolute urls
Angular HttpClient
handles protection against Cross-Site Request Forgery attacks by default, as we can read in the documentation:
https://angular.io/guide/http#security-xsrf-protection
HttpClient
supports a common mechanism used to prevent XSRF attacks. When performing HTTP requests, an interceptor reads a token from a cookie, by defaultXSRF-TOKEN
, and sets it as an HTTP header,X-XSRF-TOKEN
.
Nonetheless, we have to remember to avoid absolute paths when sending any mutable requests. The HttpXsrfInterceptor::intercept
method is documented as this:
Skip both non-mutating requests and absolute URLs.
https://github.com/angular/angular/blob/ff9f4de4f1147ba9ea6d17c31442a2eedcf4e0d2/packages/common/http/src/xsrf.ts#L77
Non-mutating requests don’t require a token, and absolute URLs require special handling anyway as the cookie set on our origin is not the same as the token expected by another origin.
With that in mind, we can decide to use relative paths in our requests. Below you can find an example based on the TaskDataService class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// frontend/src/main/angular/src/app/tasks/services/task-data.service.ts import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient } from '@angular/common/http'; import { Task } from '../task'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; @Injectable({ providedIn: 'root' }) export class TaskDataService { constructor(private http: HttpClient) { } public save(task: Task) { if(!task.id) { return this.http.post<Task>('api/tasks', task, httpOptions); } } } |
Configure proxy to a backend server
Now we need to divert all calls that use the /api
path to a server, that in my example project, exposes endpoints on http://localhost:8080/api/*
. In order to achieve that, we need to use the Angular proxy support. Create the following file in the src
directory of your Angular project:
1 2 3 4 5 6 7 8 |
// src/proxy.conf.json { "/api": { "target": "http://localhost:8080", "secure": false, "logLevel": "debug" } } |
Next, I’m going to configure the $ ng serve
command in my angular.json
to use the proxy file:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/main/angular/angular.json … "architect": { … "serve": { … "builder": "@angular-devkit/build-angular:dev-server", "options": { … "proxyConfig": "src/proxy.conf.json" }, … |
Every time you update your proxy configuration rerun the $ ng serve command to make your changes effective. |
Thanks to enabling the debug log level in my proxy.conf.json
file I can read in my logs that the target was set properly:
1 2 3 4 5 |
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** : Compiled successfully. … [HPM] POST /api/tasks -> http://localhost:8080 … |
Another key point is to read the Angular proxy documentation thoroughly in order to learn about:
- rewriting the URL path (e.g. how to remove the
/api
part from URLS); - using proxy for multiple entries;
- changing origin when your backend is not on
localhost
.
To sum up, run the $ ng serve
command and verify that the Angular app attaches the XSRF-TOKEN cookie and X-XSRF-TOKEN header to mutable requests as shown on the image below:
Furthermore, you can find all the work that I described here in the scrum-ally and jwt-spring-boot-angular-scaffolding projects.
Learn more on debugging “Invalid CSRF token found” error
- In the long run, take some time to read more on the Security: XSRF protection subject.
- You should explore The official guide on Angular Security as well.
- Visit both the HttpClientXsrfModule documentation and HttpXsrfInterceptor code.
- The Angular 9 HttpClientXsrfModule not working issue on GitHub was a tremendous help for me when I was fixing my code.
- You can find more details about proxy in this StackOverflow response.
- I found some additional useful info under the Angular 6 does not add X-XSRF-TOKEN header to http request question on StackOverflow.
- This article serves as an addendum for the Securing your Spring Boot and Angular app with JWT #3 – Frontend post.