0

I have created a small website that I would like to provide. I use Spring Boot 3 as backend and Angular 15 as frontend. Now I have like many the problem that my frontend can't communicate with my backend because of Cors. I have looked at many sites on the internet and tried to solve the problem myself but unfortunately without success (https://rajendraprasadpadma.medium.com/what-the-cors-ft-spring-boot-spring-security-562f24d705c9 , Spring Boot : CORS Issue).

Here you can see my attempts:

Attempt 1: Adding a Bean in my Security Config

       @Bean
        CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("*"));
            configuration.setAllowedMethods(Arrays.asList("*"));
            configuration.setAllowedHeaders(List.of("*"));
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }

Attempt 2: Adding another bean in my Security Config

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        //the below three lines will add the relevant CORS response headers
        configuration.addAllowedOrigin("*");
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

Attempt 3: Adding @CrossOrigin to each of my controller class

import org.springframework.web.bind.annotation.CrossOrigin;

@RestController
@RequestMapping("/api/v1/productcategory")
@CrossOrigin
public class ProductCategoryController {

    @Autowired
    ProductCategoryService productCategoryService;
    
    @GetMapping
    public ResponseEntity<List<ProductCategory>> get(){
        return ResponseEntity.ok(productCategoryService.getAll());
    }
    @PostMapping
    public ResponseEntity<String> post(){
        return ResponseEntity.ok("POST: Hello Management");
    }
    @PutMapping
    public ResponseEntity<String> put(){
        return ResponseEntity.ok("PUT: Hello Management");
    }
    @DeleteMapping
    public ResponseEntity<String> delete(){
        return ResponseEntity.ok("DELETE: Hello Management");
    }

Attempt 4: Adding the Cors property to my FilterChain

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, 
                                    @NonNull HttpServletResponse response, 
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {
        
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
        
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String userEmail;
        
        if(authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        jwt = authHeader.substring(7);
        userEmail = jwtService.extractUsername(jwt);

        if(userEmail !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
            var isTokenValid = tokenRepository.findByToken(jwt)
                                                .map(t -> !t.isExpired() && !t.isRevoked())
                                                .orElse(false);
            if(jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }

To this is to say what I use as authentication and authorization a JWT token.

In my Angular app, I have an HttpIntercept:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class RequestInterceptorInterceptor implements HttpInterceptor {

  constructor() {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    if (request.method !== 'OPTIONS') {
      const jwtString = localStorage.getItem('jwt');
      const jwtToken = jwtString ? JSON.parse(jwtString)?.jwt as string : '';
      const authorizationHeader = `Bearer ${jwtToken}`;
      request = request.clone({ headers: request.headers.set('Authorization', authorizationHeader)});
    }

    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        // Hier kannst du den Fehler abfangen und entsprechende Aktionen ausführen
        if (error.status === 0 && error.statusText === 'Unknown Error') {
          console.log('Es ist ein CORS-Fehler aufgetreten');
          // Weitere Aktionen ausführen ...
        }

        // Wirf den Fehler weiter, um ihn an den Aufrufer weiterzugeben
        return throwError(error);
      })
    );
  }
}

Here are my error logs:

enter image description here

What amazes me is that I can't even get through the preflight with my attempts.

Does anyone have any idea what I'm doing wrong or what I can still try. So that back- and frontend communicate with each other?

Thanks in advance

Doncarlito87
  • 359
  • 1
  • 8
  • 25
  • The preflight IS probably your issue - or at least part of it. Can your server handle an OPTIONS request and does it reply with a non-error response? Also, do you need credentials? If not, disable that cors rule as it will make stuff a lot stricter. – MikeOne Aug 22 '23 at 15:05
  • Also when I started Local my app then I don't have this problem. Since it had with Attempt 1 Works. If I use setAllowedMethods(Arrays.asList("*")); then options should work too. Or not? – Doncarlito87 Aug 22 '23 at 15:13
  • What is your deployment architecture ? And why your front end and back end are being served on two different domains ? – Gaurav Aug 22 '23 at 17:32
  • So I put my apps in Docker containers and deployed them in Azure Kubernetes. I currently have 2 domains because my frontend couldn't communicate with the backend at all in the beginning. Only after the change did it work. – Doncarlito87 Aug 22 '23 at 18:28
  • No! Never ever reflect arbitrary origins while also allowing credentialed access. This is wildly insecure! – jub0bs Aug 22 '23 at 19:24

1 Answers1

-1

So the solution to your problem lies sorting out your code from 'Attempt 4' i.e. your CORS filter. Looking from that piece of code, you are using doFilterInternal method, which is from Class OncePerRequestFilter.

While this method is identical to doFilter, it has one big difference, it has same contract as for doFilter, but guaranteed to be just invoked once per request within a single request thread.

And this is where the problem resides, you just need to use doFilter from javax.servlet.Filter class.

So the final look of your CORS filter class shall be something like below:

package your.own.project.package.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component
@Slf4j
public class CORSFilter implements Filter {



    public CORSFilter() {
        log.info("*************Initialising a CORS FILTER*************");
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String headerOrigin = request.getHeader("Origin");
        if (headerOrigin != null) {
            response.setHeader("Access-Control-Allow-Origin", headerOrigin);
        }

        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        //Note the `Authorization` header in this list below, 
        // this is important to allow JWT 'Autorization Bearer` token headers 
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me, Authorization");
        
        //Then the rest of your your Auth stuff as ususal
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String userEmail;
        
        if(authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }
        jwt = authHeader.substring(7);
        userEmail = jwtService.extractUsername(jwt);

        if(userEmail !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
            var isTokenValid = tokenRepository.findByToken(jwt)
                                                .map(t -> !t.isExpired() && !t.isRevoked())
                                                .orElse(false);
            if(jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

Also, please note, in the list of headers in Access-Control-Allow-Headers i've added Authorization header. If you don't have this, you'll get HTTP 400 Request, for as the Authorization header is not allowed by server.

Also, as better technical practice, i'll advice you to keep the CORS filter clean of Authentication processing.

Ideally, you should create your own dedicated custom Auth security filter class extending AbstractAuthenticationProcessingFilter class, and doing the stuffs you are doing above in overridden attemptAuthentication and successfulAuthentication method respectively.

I hope this sorts out your issue.

Abhinav Ganguly
  • 276
  • 4
  • 7
  • So I applied your changes, but unfortunately the same error comes up. I stupidly did not check that Azure did not properly pull my Docker images. Therefore I can start all my attempts again o.O – Doncarlito87 Aug 22 '23 at 18:26
  • No! This CORS configuration (reflecting arbitrary origins while allowing credentialed access) is wildly insecure! – jub0bs Aug 22 '23 at 19:23