1

I am currently working on the csrf protection inside a SPA application.

Project-Structure (Code-Snippets will be below)

In the backend I have defined a SecurityConfigurator in which I enable CSRF with Customizer defaults. I have also defined the Controller on "/api/csrf/token" which simply returns a CsrfToken. So most basic csrf implementation.

I am using openapi-generator-cli to autogenerate the services in Angular. Inside Angular I have defined an Interceptor to add the csrf-token to all request (Although not needed for all). My goal is to init application settings; done by the POST-Request to "api/initSettings".

What is working

When I request localhost:8080/api/csrf/token via Postman, I get a token object - e.g.:

{
    "parameterName": "_csrf",
    "token": "tokenXYZ",
    "headerName": "X-CSRF-TOKEN"
}

When I add this token to the X-CSRF-TOKEN header and process a POST request on localhost:8080/api/initSettings via Postman, I do receive http 200 OK and the expected response.

If I try the same steps within my application and observe the network activity, I do receive a csrf-token "tokenABC" from localhost:8080/api/csrf/token. The settings exchange does a request on localhost:8080/api/initSettings with the header X-CSRF-TOKEN and the value tokenABC.

The problem

Although the token is delivered to the backend, it results in a 403 - error

Invalid CSRF token found for http://localhost:8080/api/initSettings

inside the CsrfFilter. Debugging the code shows, that the token from the frontend is the same but it's resolved version does not equal to the deferredCsrfToken taken from the tokenRepository.

Debugger view of the init Settings request

Additional comments

First I thought I might have missed to save the token to the repository but since it is working with postman as expected, spring must have taken care about this.

Second thought has been, that it's not working, because I start the request instantly after I got the token. But with a 5 second delay between the two request it still delivers the same error.

I have stumbled upon this post that might help me find a solution: Exploit Spring Security.

Also helpful might be this StackOverflowQuestion or this article. If any of this links works for me, I will edit this post at the end.

I guess, it must be a problem either with Angular or with my browser. But I am not sure.

I will add some Cors-Config as well, although I do not think it is related.

Hopefully someone can help me, thanks in advance.

Environment

  • Angular 18.2.12
  • Spring-Boot 3.3.5
  • Spring Security 6.3.4
  • Openapi-Generator-Cli 7.10.0
  • Springdoc-openapi 2.3.0
  • Ngrx/store 18.1.1
  • Browser Chrome

Code snippets

SecurityConfigurator:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfigurator {

    private final DefaultValuesAccessor defaultValuesAccessor;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;


    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
                .anonymous(AbstractHttpConfigurer::disable)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .csrf(Customizer.withDefaults())
                .authorizeHttpRequests(getHttpUrlRules())
                .sessionManagement(getSessionRules())
                .cors(getCorsRules())
                .authenticationProvider(authenticationProvider())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    /**
     * The Cors Configurations are defined inside the application properties.
     * At the moment of this StackOverflow question, the actual values has been added as comments.
     */
    private Customizer<CorsConfigurer<HttpSecurity>> getCorsRules() {
        return corsConfigurer -> {
            corsConfigurer.configurationSource(request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(defaultValuesAccessor.getAllowedOrigins());        //["http://localhost:4200"]
                config.setAllowedMethods(defaultValuesAccessor.getAllowedHttpMethods());    //["*"]
                config.setAllowedHeaders(defaultValuesAccessor.getAllowedHeaders());        //["*"]
                return config;
            });
        };
    }

    /**
     * Defines access rules for specific paths
     */
    private Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> getHttpUrlRules() {
        return authorize -> {
            //Order matters! E.g. requestMatcher with api/** at first would change the access logic (no need to be admin then)
            authorize
                    .requestMatchers("/api/**")
                    .permitAll(); //For Testing purpose.
                    .anyRequest()
                    .denyAll();
        };
    }
}

CsrfController:

@RestController()
@RequestMapping(value = "/api",produces = MediaType.APPLICATION_JSON_VALUE)
public class CsrfController {
    @GetMapping("csrf/token")
    public CsrfToken csrfToken(CsrfToken token) {
        return token;
    }
}

InitSettingsController:

@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE)
@Slf4j
public class SettingsController {

    private final SettingsService settingsService;

    /**
     * Processes the exchange of settings with a new client.
     */
    @PostMapping("/initSettings")
    ServerSettingsDAO initSetting(@RequestBody ClientSettingsDAO clientSettings){
        log.info("Bootstrapping settings exchange:{}",clientSettings); //Not reached
        return settingsService.initSettings(clientSettings);
    }
}

CsrfInterceptor inside app.config.ts:

export function csrfInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
  // Inject the current `Tokenservice` and use it to get the csrf token:
  const csrfToken: CsrfToken = inject(TokenService).getCsrfToken();
  console.log("csrfToken:", csrfToken); //This works -> outputs the actual token from the backend
  // Clone the request to add the csrf header.
  if(csrfToken && csrfToken.token && csrfToken.headerName) {
    const newReq = req.clone({
      headers: req.headers.append(csrfToken.headerName, csrfToken.token)
    });
    return next(newReq);
  }
  return next(req);
}

Registering interceptor inside app.config.ts:

export const appConfig: ApplicationConfig = {
    providers: [
      MessageService,      provideHttpClient(withFetch(),withInterceptors([csrfInterceptor,jwtInterceptor,exceptionInterceptor])),
    ]
  };

Make requests inside app.component.ts:

constructor(public settingsStore: Store<SettingsInterface>,
    private _settingService: SettingsControllerService,
    public _tokenService: TokenService,
    private _csrfTokenService: CsrfControllerService) {
//Get CsrfToken
console.log("Initializing...");
this._csrfTokenService.csrfToken({}).subscribe(csrfToken => {
    console.log("Received Csrf Token: ", csrfToken);
    this._tokenService.setCsrfToken(csrfToken);

    this.settingsStore.pipe(select(selectClientSettings)).subscribe(clientSettings => {
        //Submit the clientSettings to the server
        this._settingService.initSetting(clientSettings).subscribe(response => {
            //Dispatch the Server Settings to the store
            this.settingsStore.dispatch(Actions.setServerSettings({serverSettings: response}));
        });
    }).unsubscribe();
});

(token.service.ts)

@Injectable({
  providedIn: 'root'
})
export class TokenService {
  private user: ClientOnlyUser = {};

  constructor(public _userStore: Store<UserInterface>) {
    this._userStore.pipe(select(selectUser)).subscribe(user => {
      this.user = user.user;
    })
  }

  getCsrfToken() {
    console.log("Inside getCsrfToken(): ", this.user.csrf);
    return this.user.csrf;
  }

  setCsrfToken(csrfToken: CsrfToken) {
    let userCopy: ClientOnlyUser = Object.assign({},this.user);
    userCopy.csrf = csrfToken;
    this._userStore.dispatch(Actions.setUser({user: userCopy}));
  }
}

Edit:
I've found out the reason for the error is that my session management is Stateless - Leading to no Session for the request. The crsf Token does need a Session by default. I've decided to leave the application without csrf protection duo to this post: Reson why csrf is not necessary when building a stateless application

2
  • I dont remember what was the cause of it but when you send a csrf token in the payload to the client side, that token is not valid for the next request in server. idea of the csrf token you receive it in the headers of the server response and you send it back with your next request. so header token value is different than payload value, so they dont match. Also think in that way, you need one csrf token per request. so if you receive 2, one of them should be not usable Commented Feb 19 at 9:55
  • Hallo @DenizKaradağ, unfortunattly this did not resolve the probelm. But I did found out more about the problem inside my application. The reason, why the tokens do not match inside my application is, that i have defined the session management to Stateless and I do not manage the session. So the csrf cookie is safed to no session and can't be loaded within the next request - therefore Spring will create a new csrf-token. The reason why it is wokring with postman is because postman actually manages session by default (via cookies). Commented Feb 20 at 14:56

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.