Tracking down how to fix CORS issue

4.3k Views Asked by At

I have had a CORS issue in an application I'm working on.

It's setup in Kubernetes, with a third party Java framework:

http://www.ninjaframework.org/

I am getting the following error:

Preflight response is not successful
XMLHttpRequest cannot load https://api.domain.com/api/v1/url/goes/here? due to access control checks.
Failed to load resource: Preflight response is not successful

I don't think the problem is in Kubernetes, but just in case - here's my Kubernetes setup:

apiVersion: v1
kind: Service
metadata:
  name: domain-server
  annotations:
    dns.alpha.kubernetes.io/external: "api.domain.com"
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-2:152660121739:certificate/8efe41c4-9a53-4cf6-b056-5279df82bc5e
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
spec:
  type: LoadBalancer
  selector:
    app: domain-server
  ports:
    - port: 443
      targetPort: 8080
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: domain-server
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3
  revisionHistoryLimit: 10
  template:
    metadata:
      labels:
        app: domain-server
    spec:
      containers:
        - name: domain-server
          image: "location.aws.etc"
          imagePullPolicy: Always
    ...

I am totally lost here - how do I enable CORS on my api endpoints? I'm sorry if this is a simple question or I haven't provided enough information here, but I have no clue how to do this and I've tried several pathways.

Note, just to be clear, api.domain.com is a replacement for my actual api domain, I just don't want to reveal what site I am working on

EDIT:

My guess is that it might have something to do with this:

private Result filterProtectedApi(FilterChain chain, Context context, boolean isMerchant, JwtAuthorizer jwtAuthorizer) {
    String authHeader = context.getHeader("Authorization");
    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
        return this.forbiddenApi();
    }
    context.setAttribute("access-token", authHeader.substring("Bearer ".length()));
    return this.filterProtected(chain, context, isMerchant, jwtAuthorizer, parser -> parser.parseAuthHeader(authHeader), this::forbiddenResource);
}

private AuthLevel getAuthLevel(String requestPath) {
    log.info("REQUEST PATH: " + requestPath);
    if (requestPath.equals("/auth") || requestPath.equals("/auth/merchant") || requestPath.equals("/auth/app")
            || requestPath.startsWith("/assets/") || requestPath.equals("/privacy-policy.html")
            || requestPath.equals("/forbidden.html") || requestPath.equals("/favicon.ico")
            || requestPath.startsWith("/invite/ios/") || requestPath.startsWith("/stripe/")
            || requestPath.startsWith("/chat")) {
        return AuthLevel.UNPROTECTED_RESOURCE;
    }
    if (requestPath.startsWith("/merchant/api/")) {
        return AuthLevel.PROTECTED_MERCHANT_API;
    }
    if (requestPath.startsWith("/merchant/")) {
        return AuthLevel.PROTECTED_MERCHANT_RESOURCE;
    }
    if (requestPath.startsWith("/api/")) {
        return AuthLevel.PROTECTED_API;
    }
    return AuthLevel.PROTECTED_RESOURCE;
}

I have tried adding something to ignore OPTIONS requests, but I still get failed the preflight check

private Result filterProtectedApi(FilterChain chain, Context context, boolean isMerchant,
        JwtAuthorizer jwtAuthorizer) {
    if (context.getMethod().toLowerCase().equals("options")) {
        return chain.next(context);
    }
    String authHeader = context.getHeader("Authorization");
    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
        return this.forbiddenApi();
    }
    context.setAttribute("access-token", authHeader.substring("Bearer ".length()));
    return this.filterProtected(chain, context, isMerchant, jwtAuthorizer,
            parser -> parser.parseAuthHeader(authHeader), this::forbiddenResource);
}

What do I need to do to have the preflight check succeed?

EDIT - changed it to this per advice below:

@Override
public Result filter(FilterChain chain, Context context) {
    if (context.getMethod().toLowerCase().equals("options")) {
        return Results.html().addHeader("Access-Control-Allow-Origin", "*")
                .addHeader("Access-Control-Allow-Headers", "Authorization")
                .addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS").render("OK");
    }
    AuthLevel authLevel = this.getAuthLevel(context.getRequestPath());
    switch (authLevel) {
    case PROTECTED_API: {
        return this.filterProtectedApi(chain, context, false, this.jwtAuthorizer);
    }
    case PROTECTED_MERCHANT_RESOURCE: {
        return this.filterProtectedResource(chain, context, "merchant-access-token", "/auth/merchant", true,
                this.merchantJwtAuthorizer);
    }
    case PROTECTED_MERCHANT_API: {
        return this.filterProtectedApi(chain, context, true, this.merchantJwtAuthorizer);
    }
    case UNPROTECTED_RESOURCE: {
        return this.filterUnprotectedResource(chain, context);
    }
    }
    return this.filterProtectedResource(chain, context, "access-token", "/auth", false, this.jwtAuthorizer);
}
2

There are 2 best solutions below

8
Jannes Botis On

You are on the right path, trying to ignore OPTIONS requests before the auth validation:

if (context.getMethod().toLowerCase().equals("options")) {
    return chain.next(context);
}

What is needed furthermore is to respond properly to a preflight request:

if (context.getMethod().toLowerCase().equals("options")) {
    return Results.html()
                  .addHeader("Access-Control-Allow-Origin", "*")
                  .addHeader("Access-Control-Allow-Headers", "Authorization")
                  .addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE")
                  .render("OK");
}

In short, you need to respond with

  • an appropriate http status code, typically 200 or 204
  • add the needed http response headers:
    • "Access-Control-Allow-Origin" with either "*" to allow CORS from all domains or "http://www.domainA.com" to allow only from a specific domain
    • "Access-Control-Allow-Headers", http headers allowed
    • "Access-Control-Allow-Methods", http methods allowed
  • Response body is irrelevant, you can just send "OK".

Note that a Preflight request can be done from any route, so I would suggest to create a new filter with the code above and use it for all routes before any others.

So you use it after implementing the filter() method:

public Result filter(FilterChain chain, Context context) {
     if (context.getMethod().toLowerCase().equals("options")) {
          return Results.html()
                  .addHeader("Access-Control-Allow-Origin", "*")
                  .addHeader("Access-Control-Allow-Headers", "Authorization")
                  .addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE")
                  .render("OK");
     }

CORS on Kubernetes Ingress Nginx

Try to enable CORS at annotations config:

annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"
    nginx.ingress.kubernetes.io/cors-allow-origin: "http://localhost:8100"
    nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
    nginx.ingress.kubernetes.io/cors-allow-headers: "authorization"

Be aware that the string "*" cannot be used for a resource that supports credentials (https://www.w3.org/TR/cors/#resource-requests), try with your domain list (comma separated) instead of *

References:

0
Sreram Balasubramaniyan On

You are actually mixing two things here. Access control and Cross origin requests.

Cross origin requests can be handled directly by Kubernetes. You need to configure your ingress appropriately to forward the cross origin requests properly. No need to configure anything in your application. For sample configuration, see here.

However, access control (Authentication and Authorization) needs to be handled at the application level, for which such filters can be used. If you use options for some functionality, only then it needs to be handled and implemented. My personal suggestion would be to directly filter those requests.

If you mix both Cross-origin / proxy requests and Access control, you will keep facing one issue or the other all the time. Let the individual modules do what they are supposed to do, that way it is easier to debug and manage.