3
votes

I secured my Spring Boot application with Keycloak 11.0.2 and Spring Security following this documentation.

I used the basic Keycloak configuration in application.properties:

    keycloak.auth-server-url=http://localhost:8085/auth
    keycloak.realm=cirta
    keycloak.resource=cirta-api
    keycloak.public-client=false

I have a separate frontend Angular app, that is configured as a different client in Keylocak; but in the same realm as the Spring Boot app. From the Angular app I am sending the Keycloak-provided token in the HTTP headers with:

'Authorization' : 'Bearer ' + this.securityService.kc.token

When I access an Angular page that calls a GET API, I get a blocked by CORS policy error:

Access to XMLHttpRequest at 'http://localhost:8080/api/modePaiements' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

So I've tried adding the keycloak.cors=true property to application.properties. With that property added, the GET calls are working. But now when I call a POST/PUT API I'm getting a Failed to load resource: the server responded with a status of 403 () error.

KeycloakWebSecurityConfigurerAdapter:

@Override
protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http.authorizeRequests().antMatchers("/api/*").hasRole("app-manager").anyRequest().permitAll();
}

Spring Sample Application : https://github.com/bilaldekar/kc

Angular Sample Application : https://github.com/bilaldekar/kc-ang

Request Headers:

enter image description here

1
Comments are not for extended discussion; this conversation has been moved to chat. - Samuel Liew♦
How this.securityService.kc.token decoded payload looks like? Is there app-manager role? - Jan Garaj
i added the token in the question, yes i created a role app-manager. - Billy DEKAR
@deduper can you give the config that resolved the 401/403, i will test it with frontend calls and see if it works - Billy DEKAR
i found out that a backend api should be configured as bearer only, not a public client, so that access to an api is given by the token sent from frontend, but this didn't resolve the issue - Billy DEKAR

1 Answers

0
votes

Looks to me a csrf issue.

Add the following to the security config

http.authorizeRequests().antMatchers("/api/*").hasRole("app-manager").anyRequest().permitAll()
                .and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

The above code will set the CSRF token in the cookie named XSRF-TOKEN. Angular reads this cookie for CSRF tokens by default and adds this in the subsequent requests. To make sure Angular can read it, we are setting the Http only to False. This has its own security implications as the CSRF token can be accessed via script now. If you don't prefer to do this, the other way would be to read the X-XSRF-TOKEN token from the response header and send it in the subsequent request header using Angular interceptor.

Update:

When testing locally, where Angular is running on 4200, the Spring Boot app, the Keycloak server on 8080, 8085 ports.

Use the Webpack Dev server proxy with the following steps. (You don't need this config anymore keycloak.cors=true)

in the environment.ts update the apiUrl

apiUrl: '', //Spring Boot API

then add a proxy.conf.json under the src folder with the following content

{
    "/api": {
        "target": "http://localhost:8080",
        "secure": false
    },
    "logLevel": "debug"
}

Add the proxy config in the angular.json serve command

"options": {
    "browserTarget": "kc-ang:build",
    "proxyConfig": "src/proxy.conf.json"
}

Now you would notice the api requests would go to localhost:4200/api/* and that would be routed to the spring boot app via the above config.

With this the XSRF related cookies and the header would be part of the request and you should not get any issues.

[![Sample working version][1]][1]

Update 2: Support CORS

If your frontend and backend are on different domains and if you must support CORS, then this is what needs to be done

The security config on Spring should allow CORS for the origin of the frontend (in this case http://localhost:4200):

protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http.authorizeRequests().antMatchers("/api/*").hasRole("app-manager").anyRequest()
            .permitAll()
    .and().csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .and().cors();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
    configuration.setAllowedMethods(Arrays.asList("GET","POST"));
    configuration.setAllowCredentials(Boolean.TRUE);
    configuration.addAllowedHeader("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/*", configuration);
    return source;
}

On Angular, update the apiUrl in environment.ts file to

apiUrl: '//localhost:8085'

While making any write operations (POST/PUT/DELETE) using the Angular http client, set the withCredentials option to true. (Also ensure the method is supported for CORS on the server side config. In the above CORS config we have allowed GET and POST only. If you need to support PUT/DELETE ensure you update the CORS configuration accordingly)

In this case update the following method in fournisseur.service.ts

addFournisseur(fournisseur: Fournisseur): Observable<Fournisseur> {
    return this.httpClient.post<Fournisseur>(this.envUrl + '/api/fournisseurs', fournisseur, {withCredentials: true});
  }