gateway: JWT SecurityPolicy blocks CORS preflight requests, breaking OIDC + JWT flow

Description: the JWT configuration in the Security filter does not include the bypass_cors_preflight configuration option. Without this setting, OPTIONS requests are blocked unless the JWT is provided.

If the JWT is being passed through cookies with a standard OIDC frontend / JWT backend setup then the credentials won’t be included since the preflight request needs to include the Access-Control-Allow-Credentials: true response header` before credentials will be sent.

I think bypass_cors_preflight should be set to true by default. I can’t think of a good reason why someone would want to require a JWT for OPTIONS requests since Envoy doesn’t even send OPTIONS requests down to the backend service. As a result the backend application can’t customize the response. So blocking by default will just confuse people while providing no real benefit.

About this issue

  • Original URL
  • State: open
  • Created 6 months ago
  • Comments: 16 (11 by maintainers)

Most upvoted comments

@apjoseph @arkodg

I tested the below SecurityPolicy with cors and jwt, and it worked.

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: cors-example
  namespace: default
spec:
  cors:
    allowHeaders:
    - x-header-1
    - x-header-2
    allowMethods:
    - GET
    - POST
    allowOrigins:
    - type: RegularExpression
      value: .*\.foo\.com
    exposeHeaders:
    - x-header-3
    - x-header-4
  jwt:
    providers:
    - name: example
      remoteJWKS:
        uri: https://raw.githubusercontent.com/envoyproxy/gateway/main/examples/kubernetes/jwt/jwks.json
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: backend

Use curl to send a CORS preflight request with the origin http://www.foo.com, and it succeeded.

$ curl -H "Origin: http://www.foo.com" \
>   -H "Host: www.example.com" \
>   -H "Access-Control-Request-Method: GET" \
>   -X OPTIONS -v -s \
>   http://$GATEWAY_HOST/ \
>   1> /dev/null
*   Trying ::1:8888...
* TCP_NODELAY set
* Connected to localhost (::1) port 8888 (#0)
> OPTIONS / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/7.68.0
> Accept: */*
> Origin: http://www.foo.com
> Access-Control-Request-Method: GET
> 
Handling connection for 8888
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< access-control-allow-origin: http://www.foo.com
< access-control-allow-methods: GET, POST
< access-control-allow-headers: x-header-1, x-header-2
< access-control-expose-headers: x-header-3, x-header-4
< date: Fri, 22 Dec 2023 02:56:29 GMT
< server: envoy
< content-length: 0
< 
* Connection #0 to host localhost left intact

Tried to use another origin http://www.bar.com, and it failed with 401 Unauthorized, which is expected because this origin doesn’t match the CORS setting in the SecuritPolicy, and it’s passed down to the JWT filter.

$ curl -H "Origin: http://www.bar.com" \
>   -H "Host: www.example.com" \
>   -H "Access-Control-Request-Method: GET" \
>   -X OPTIONS -v -s \
>   http://$GATEWAY_HOST \
>   1> /dev/null
*   Trying ::1:8888...
* TCP_NODELAY set
* Connected to localhost (::1) port 8888 (#0)
> OPTIONS / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/7.68.0
> Accept: */*
> Origin: http://www.bar.com
> Access-Control-Request-Method: GET
> 
Handling connection for 8888
* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< www-authenticate: Bearer realm="http://www.example.com/"
< content-length: 14
< content-type: text/plain
< date: Fri, 22 Dec 2023 03:13:46 GMT
< server: envoy

In this comment, I followed the user doc to test, but the demo SecurityPolicy in the user doc is wrong, the cors setting in the user doc is

  cors:
    allowOrigins:
    - type: Exact
      value: "www.foo.com"

but the test command is

curl -H "Origin: http://www.foo.com" \
  -H "Host: www.example.com" \
  -H "Access-Control-Request-Method: GET" \
  -X OPTIONS -v -s \
  http://$GATEWAY_HOST \
  1> /dev/null

Since the http://www.foo.com origin in the test command doesn’t match the www.foo.com in the SecurityPolicy, the CORS filter ignores this OPTION request and passes it down to the JWT filter. That’s why it gets a 401 Unauthorized response.

My bad on the confusing doc 😃. I’m the one who wrote it. I’ll go ahead to raise a PR to clean things up.

@apjoseph I suspect your preflight request failed for a similar reason. Could you please try to verify it with curl and share the SecurityPolicy and the curl command and result?

@zhaohuabing I think its worth further debugging https://github.com/envoyproxy/gateway/issues/2312#issuecomment-1866051928 to understand why the OPTIONS request is getting a 401 in your test above instead of returning after hitting the cors filter because it doesnt match with https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/cors_filter#cors

@zhaohuabing Another potential solution is to allow different security policies to be applied when a different method is specified with the same path. This was actually my attempted workaround when I first encountered the issue. I created two http routes:

api-route

        matches:
          - path:
              type: PathPrefix
              value: /

api-route-cors

        matches:
          - path:
              type: PathPrefix
              value: /
           method: OPTIONS

and then created corresponding SecurityPolicies for both HttpRoutes. I only included the JWT section in the SecurityPolicy for api-route. I assumed this would prevent OPTIONS requests from being blocked; however when looking at the response from egctl I found that it was being applied to both HttpRoutes BUT only in some instances. Other times it would remove JWT auth from BOTH HttpRoutes -depending on which security policy was applied last.

I am not sure whether this is intended behavior or not, but it was counter intuitive to me as I would have expected all attributes of a match to be taken into account when applying a security policy if they are in separate routes -especially since the security policy is explicitly bound to an HTTPRoute

I like your approach of just turning the bypass_cors_preflight on by default though. It would be highly annoying to need to configure separate HttpRoute objects just for CORS.