6

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);
}
Steven Matthews
  • 9,705
  • 45
  • 126
  • 232
  • I bet that https://api.name.com/api/v1/url/goes/here does not correspond to your actual API endpoint right? so you may have missed some configuration on you web front-end. CORS is not the issue here – Benjamin Caure Jul 29 '19 at 21:27
  • It does. api.name.com is a replacement for my domain. I don't want to reveal what the actual domain is. I'll change it to domain to make that clearer. – Steven Matthews Jul 29 '19 at 21:28
  • My strong guess is that something like 'if (context.getMethod() == "OPTION") { return chain.next(context);}' will essentially green light all preflights – Steven Matthews Jul 29 '19 at 22:07
  • Nope, that wasn't it – Steven Matthews Jul 29 '19 at 22:28
  • Please show the actual browser console log output (firefox or chrome). This is not related to kubernetes, but the same origin policy and CORS checks in the browser. – Thomas Jul 30 '19 at 09:06
  • The actual browser console log output is the first thing listed. And I agree. It's not Kubernetes related. It's more than likely due to my application – Steven Matthews Jul 30 '19 at 13:51
  • If api.domain.com is different from front end url, your webapp should enable CORS for this url. For example https://www.baeldung.com/spring-cors – Benjamin Caure Jul 31 '19 at 06:59
  • Can you add a sample request(headers, method, origin) that is preflighted? Are there more request headers, apart from "Authorization" that should be added to "Access-Control-Allow-Headers"? – Jannes Botis Aug 02 '19 at 20:56
  • if you use spring and oAuth 2 you need to care for the order of the filter chain. There is a nice blog entry about it: https://medium.com/@muiruri/spring-oauth2-and-cors-configuration-3529337525b4. Just leaving this here since it helped me when I thought that I did need cors for my oAuth authorization. But after having a closer look and realizing there is no need for cors in redirects I threw that away again. :) -> you also should give this a read if you haven't already https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS (It's good to know what you are dealing with) – Markus Schreiber Aug 03 '19 at 09:38
  • try to update your ingress file using this `nginx.ingress.kubernetes.io/enable-cors: "true"` or check this solution `https://stackoverflow.com/questions/51744536/cors-rules-nginx-ingress-rules?answertab=active#tab-top` – Doctor Who Aug 06 '19 at 12:24
  • lots of advice here, can you add the result of a OPTIONS request to the question? it should be as simple as `curl -v -X OPTIONS --header 'Access-Control-Request-Method: GET' --header 'Access-Control-Request-Headers: ' https://api.domain.com/api/v1/url/goes/here`. From there we can work out what needs to be fixed – stringy05 Aug 07 '19 at 02:27
  • So the request to `https://api.domain.com/api/v1/url/goes/here?` failed, but what was the website url domain? Is it the same(api.domain.com)? – Samuel Negri Aug 07 '19 at 11:29
  • Also please check whether you have imported the right class. I've experienced it. – Alexpandiyan Chokkan Aug 07 '19 at 15:55

2 Answers2

5

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:

Jannes Botis
  • 11,154
  • 3
  • 21
  • 39
  • Should I not worry about the chain.next(context) then? – Steven Matthews Aug 01 '19 at 15:25
  • No, you do not need to continue to any other filter. A Options response should just "tell" the browser which origin(s), request headers and methods are permitted. The response body is irrelevant. – Jannes Botis Aug 01 '19 at 15:38
  • I just tested this, and I am still getting CORS issues, so I'm guessing I didn't use it in the correct place – Steven Matthews Aug 01 '19 at 16:37
  • Better define a new filter and use it for all routes, not only the protected ones, a preflight OPTIONS request can be send from any route. This filter should be 1st also. – Jannes Botis Aug 01 '19 at 16:41
  • Is it better to add another filter rather than putting the conditions to check for option requests first before any of the routing for the rest of the requests? – Steven Matthews Aug 01 '19 at 16:47
  • I am a little blind on how "filterProtectedApi" is used(when is called), you may have to use the check for an OPTIONS request at the beginning of your filter when implementing `public Result filter(FilterChain chain, Context context) {` – Jannes Botis Aug 01 '19 at 16:51
  • Try to refresh your browser a few times also, make sure the preflight request is not cached. – Jannes Botis Aug 01 '19 at 17:05
  • It's a Cordova app. I just deleted the whole thing and recreated it (so no persistence), and it's still happening. So it appears it's not Kubernetes, it's not Ninja (maybe?) - where the hell can CORS be causing an issue here? I am trying to use this plugin: https://github.com/ionic-team/cordova-plugin-ionic-webview – Steven Matthews Aug 01 '19 at 17:08
0

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.