I am currently working on the csrf protection inside a SPA application.
Project-Structure (Code-Snippets will be below):
At 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 at 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 recieve an 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 recieve 200 OK and the expected response.
If I try the same steps within my application and observe the network activity, I do recieve an 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 defferedCsrfToken taken from the tokenRepository.
Additional comments:
First i thought I might have missed to save the token to the repository but since it is wokring with postman as aspected, 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 uppon 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.
Enviroment:
*emphasized text* **Code-Snippets:**
*ScurityConfigurator:*
@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 appponent.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}));
}
}