2

I have UI written with Angular 2 and Java based backend that uses OpenID Connect authentication on top of Spring Security.

The authentication works fine but only for GET requests. I'm getting HTTP 403 every time I perform POST, PUT or DELETE methods on a resource:

{
  "status": 403,
  "message": "Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.",
}

I use HttpClient like this:

http.post(
    '/api/my-resource',
    JSON.stringify(myObject),
    new RequestOptions({
        headers: new Headers({'Content-Type': 'application/json'})
    })
)

When I add withCredentials: true to the RequestOptions as proposed here I still get HTTP 403 but with different message:

{
  "status": 403,
  "message": "Could not verify the provided CSRF token because your session was not found.",
}

What do I need to do to make HttpClient work with CSRF?

Note: I also took a look at similar questions, especially angular-2-spring-security-csrf-implementation-problems, but the solutions proposed there do not solve my problem. At the same time I don't want to disable CSRF.

Sasha Shpota
  • 9,436
  • 14
  • 75
  • 148

2 Answers2

4

Update Angular >= 6

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    HttpClientXsrfModule.withOptions({cookieName: 'XSRF-TOKEN'})
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Original answer

Add the CSRF cookie configuration as below :

@NgModule({
    declarations: [declarations],
    imports: [imports],
    providers:
    [
        {provide: XSRFStrategy, useFactory: xsrfFactory}
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}

export function xsrfFactory() {
    return new CookieXSRFStrategy('XSRF-TOKEN', 'XSRF-TOKEN');
}

And configure Spring security with the correct header name.

    private static final String CSRF_HEADER_NAME = "XSRF-TOKEN";

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            ...
            .and()
                .csrf()
                    .ignoringAntMatchers(CSRF_IGNORE)
                    .csrfTokenRepository(csrfTokenRepository())
            .and()
               .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
    }

    private CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName(CSRF_HEADER_NAME);
        return repository;
    }

where CsrfHeaderFilter :

public class CsrfHeaderFilter extends OncePerRequestFilter {

    private static final String CSRF_COOKIE_NAME = "XSRF-TOKEN";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());

        if (csrf != null) {

            Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME);
            String token = csrf.getToken();

            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                cookie = new Cookie(CSRF_COOKIE_NAME, token);
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        }

        filterChain.doFilter(request, response);
    }
}
Community
  • 1
  • 1
Radouane ROUFID
  • 10,595
  • 9
  • 42
  • 80
  • Thank you Radouane! I guess `.ignoringAntMatchers(CSRF_IGNORE)` is not needed in my case, is it? – Sasha Shpota Jul 21 '17 at 12:14
  • You can add all the URL (wilcards) that you want to be ignored for CSRF. If you have a login URL for example. You can skip the line if nothing to ignore. – Radouane ROUFID Jul 21 '17 at 12:16
  • May be I'm doing something wrong but I'm still getting `Invalid CSRF Token 'null' was found on the request parameter 'XSRF-TOKEN' or header 'XSRF-TOKEN'.`. I see that java related part works - the returned message contains updated header name and parameter name (I also added parameter name in the `csrfTokenRepository`). However there is no corresponding header in the request generated by browser. Do you have any idea? May be I have to change the http call itself? – Sasha Shpota Jul 21 '17 at 13:19
  • Okay, I figure out that my solution is missing something. Spring Security send a header, and Angular is expecting a Cookie. I will edit my answer. – Radouane ROUFID Jul 21 '17 at 13:22
  • Thanks mate! Works like a charm. – Sasha Shpota Jul 21 '17 at 13:51
  • This doesn't seem to work for me :( got a q here about it - [link](https://stackoverflow.com/questions/45765990/angular-4-cant-set-csrf-token-in-header) – Maj Aug 19 '17 at 11:40
1

With spring-boot 2.2.2 (spring-security 5.2.1) and Angular 8.2.14, this configuration works for me:

  @NgModule({
  declarations: [],
  imports: [
    BrowserModule,
    HttpClientModule,
    HttpClientXsrfModule.withOptions({cookieName: 'XSRF-TOKEN'}),

The same configuration as the updated response of Radouane.

But for the springboot part:

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.csrf()
         .csrfTokenRepository(cookieCsrfTokenRepository())
         .and()
         .authorizeRequests()
         .antMatchers("/static/**").permitAll()
         ...
         .anyRequest().authenticated();
}

private CookieCsrfTokenRepository cookieCsrfTokenRepository() {

    final CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    cookieCsrfTokenRepository.setCookiePath("/");
    return cookieCsrfTokenRepository;
}

According to this answer,

Angular will add the X-XSRF-TOKEN header only if the XSRF-TOKEN cookie was generated server-side with the following options:

Path = /
httpOnly = false

Besides, the Angular app and the URL being called must reside on the same server.

F. Geraerts
  • 1,150
  • 17
  • 23