Angular/Spring Boot with Keycloak throws 403
Asked Answered
D

1

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

Devy answered 23/9, 2020 at 10:4 Comment(7)
Comments are not for extended discussion; this conversation has been moved to chat.Mackay
How this.securityService.kc.token decoded payload looks like? Is there app-manager role?Scanner
i added the token in the question, yes i created a role app-manager.Devy
@deduper can you give the config that resolved the 401/403, i will test it with frontend calls and see if it worksDevy
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 issueDevy
…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 and your AuditorAware question.Scholar
Could you show us the definition on Keycloak of app-manager role?Josefajosefina
U
-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});
  }
Unlock answered 13/10, 2020 at 12:39 Comment(16)
Can you disable CSRF entirely and check. This should at least help you eliminate a possibility.Unlock
how can i disable csrf, in angular or in spring?Devy
You need to disable on Spring. In the configure method call the disable method. http.csrf().disable();Unlock
so should i keeo http.csrf().disable(); in my config, is this secure?Devy
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.Unlock
no with the config in my question, i'm not getting XSRF-TOKEN in the cookiesDevy
I mean the one suggested in this answer i.e. with .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())Unlock
yes i'm getting XSRF-TOKEN with value b7ad0b6e-3e62-4c66-8fda-5d3d389a442aDevy
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 headersUnlock
@Unlock I was facing similar issue, this saved my day. Thanks buddy.Nealson
but the 403 error still persists when i add .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()), it only work when i add http.csrf().disable();Devy
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.Unlock
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.Unlock
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 angular.io/guide/build#proxying-to-a-backend-serverUnlock
i just added the request headers on the question, i don't think angular is sending xsrf-tokenDevy
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!Unlock

© 2022 - 2024 — McMap. All rights reserved.