4

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

Bilal Dekar
  • 3,200
  • 4
  • 28
  • 53
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/222752/discussion-on-question-by-billy-dekar-angular-spring-boot-with-keycloak-throws-4). – Samuel Liew Oct 09 '20 at 02:01
  • How `this.securityService.kc.token` decoded payload looks like? Is there `app-manager` role? – Jan Garaj Oct 10 '20 at 21:59
  • i added the token in the question, yes i created a role app-manager. – Bilal Dekar Oct 11 '20 at 08:06
  • @deduper can you give the config that resolved the 401/403, i will test it with frontend calls and see if it works – Bilal Dekar Oct 11 '20 at 10:11
  • 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 – Bilal Dekar Oct 11 '20 at 10:21
  • „*…can you give the config that resolved the 401/403…*“ — I've opted to bow out of this bounty; in deference to your own expertise, @BillyDEKAR. Best of luck to you, though, with your [*Angular question*](https://stackoverflow.com/a/63879805) and [*your AuditorAware question*](https://stackoverflow.com/q/64153617). – deduper Oct 11 '20 at 20:14
  • Could you show us the definition on Keycloak of `app-manager` role? – Davide Martorana Oct 12 '20 at 17:49

1 Answers1

-1

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});
  }
GSSwain
  • 5,787
  • 2
  • 19
  • 24
  • Can you disable CSRF entirely and check. This should at least help you eliminate a possibility. – GSSwain Oct 14 '20 at 09:24
  • how can i disable csrf, in angular or in spring? – Bilal Dekar Oct 14 '20 at 09:28
  • You need to disable on Spring. In the configure method call the disable method. http.csrf().disable(); – GSSwain Oct 14 '20 at 10:03
  • so should i keeo http.csrf().disable(); in my config, is this secure? – Bilal Dekar Oct 14 '20 at 10:32
  • 1
    If your api is going to be accessed by servers only i.e. backend to backend model then you can. If it is going to be accessed via front-end which is in this case, you should not. Would you check if you are getting the 'XSRF-TOKEN' cookie in the response with the configuration mentioned in this answer. If yes, then need to check the Angular side only (which I'll also take a look now). If no, then we have to see the Spring side of the CSRF config as well. – GSSwain Oct 14 '20 at 10:41
  • no with the config in my question, i'm not getting XSRF-TOKEN in the cookies – Bilal Dekar Oct 14 '20 at 10:47
  • I mean the one suggested in this answer i.e. with .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) – GSSwain Oct 14 '20 at 10:50
  • yes i'm getting XSRF-TOKEN with value b7ad0b6e-3e62-4c66-8fda-5d3d389a442a – Bilal Dekar Oct 14 '20 at 10:58
  • Perfect, Spring side config is sorted now. Next time when you make any Post/Put/Delete requests from Angular can you check if it is sending the `X-XSRF-TOKEN` in the request headers – GSSwain Oct 14 '20 at 11:08
  • @GSSwain I was facing similar issue, this saved my day. Thanks buddy. – Mahesh_Loya Oct 14 '20 at 11:50
  • but the 403 error still persists when i add .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()), it only work when i add http.csrf().disable(); – Bilal Dekar Oct 14 '20 at 12:19
  • What I understood from our conversation above, now the server sends the xsrf cookie. We need to check if Angular sends the X-XSRF-TOKEN in the headers for any subsequent POST/PUT/DELETE requests from the frontend after setting the Xsrf cookie. Can you share the request headers for any write operation(Post/Put/Delete) made from Angular. – GSSwain Oct 14 '20 at 13:34
  • Note: if Angular sends the X-XSRF-TOKEN and it still doesn’t work, then make sure you allow the header to be sent in your Spring CORS config. – GSSwain Oct 14 '20 at 13:37
  • Finally, if you are allowing CORS only for local development and on production, the frontend and api would be on the same domain, then you don't need to enable CORS on Spring and should use proxy for your backend via Angular using https://angular.io/guide/build#proxying-to-a-backend-server – GSSwain Oct 14 '20 at 13:40
  • i just added the request headers on the question, i don't think angular is sending xsrf-token – Bilal Dekar Oct 14 '20 at 13:46
  • Updated answer with setting up a proxy with Angular to skip the CORS based CSRF. Also added the required changes to be done if CORS is the only way for you to access your api! – GSSwain Oct 15 '20 at 05:21