Envoy: ext_authz: Add decoded OAuth2 JWT token

Created on 7 Feb 2020  路  16Comments  路  Source: envoyproxy/envoy

Title: ext_authz: Add decoded OAuth2 JWT token

Description:
Please add the decoded JWT token to the metadata that is send as input to the external authorization daemon like e.g. Open-Policy-Agent.

This feature does not need to be enabled by default but a switch would be nice.

It would help to test Open-Policy-Agent (OPA) Polices as the relevant input for the rules would then be plaintext and easy to fake to test several variants of a request.
Currently we have to extract attributes (called "claims") from the JWT using the functions provided by the OPA rule language. This has the drawback that the JWT gets verified which makes hand crafted input impossible and will eventually fail even for once valid requests when the lifetime of the tokens ends.

[optional Relevant Links:]
This is how Gluu does it, see "request_token_data" at https://gluu.org/docs/gg/plugin/gluu-opa-pep/

areext_authz enhancement help wanted

All 16 comments

This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in the next 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions.

Please can you tag it at least with "help wanted" so that it does not get closed?

@lathspell sorry I don't really understand this. Could you clarify who decodes the JWT token? Then you want envoy (the ext_authz) to send decoded JWT to OPA service in a specific format?

And if you can draw a simple flow diagram, I'll be very thankful.

I try to explain:
Envoy has a nice feature (ext_authz) that allows it to ask a "policy server" like Open Policy Agent if a certain request should be permitted or denied. To do so, it sends the request header and some other meta information JSON encoded to the policy agent and awaits a true/false decision.

As you only need such policy agents in bigger setups, the chances that you also use a protocol like OAuth2 for authentication are pretty high.

The policy agent usually wants to know which user made the request. This information is stored in the OAuth2 JWT which is part of the request (either as cookie or authorization header). The policy agent must therefore decode the JWT and will, depending on the library it uses, automatically check the signature and time-to-life dates within the JWT.

Those checks are were not only already done by envoy, they also make writing tests for the policy agent hard because one has to encode the user information in a valid JWT for the tests.

My idea now was that envoy, which already verified and parsed the JWT, stores the middle part with the user information as part of the JSON that it passes to the policy agent.
Then the policy agent could act only on the plain information that it receives without having to bother with validating JWT fields and signatures. Also my policy unit tests would only be plain JSON that is easy to write and read.

Example:

    service.auth.v2.AttributeContext.HttpRequest
    {
      "id": "...",
       "method": "...",
      "headers": "{...}",
      "jwt": "{ ... }",            <-------------- base64 decoded JWT
      "path": "...",
   ...
    }

The Gluu proxy already behaves that way as seen in the link I posted.

I think if you exercise jwt_authn before ext_authz, and you set the payload_in_metadata of that jwt_authn*, you can then set the metadata_context_namespaces of the ext_authz with:

metadata_context_namespaces:
- envoy.filters.http.jwt_authn

So ext_authz will send that jwt_authn payload to the authorization-server (in your case OPA server).

*For example, if payload_in_metadata is my_payload:

envoy.filters.http.jwt_authn:
  my_payload:
    iss: https://example.com
    sub: [email protected]
    aud: https://example.com
    exp: 1501281058

The JWT contents are indeed copied to the metadata, thanks!

Although, it does look almost like a bug that a simple JSON structure like:

        "demo-app-react": {
          "roles": [
            "users"
          ]
        },

gets converted into this monster here (you may have to scroll to the right):

opa_1         |                                                                                   [
opa_1         |                                                                                     {
opa_1         |                                                                                       "type": "string",
opa_1         |                                                                                       "value": "demo-app-react"
opa_1         |                                                                                     },
opa_1         |                                                                                     {
opa_1         |                                                                                       "type": "object",
opa_1         |                                                                                       "value": [
opa_1         |                                                                                         [
opa_1         |                                                                                           {
opa_1         |                                                                                             "type": "string",
opa_1         |                                                                                             "value": "Kind"
opa_1         |                                                                                           },
opa_1         |                                                                                           {
opa_1         |                                                                                             "type": "object",
opa_1         |                                                                                             "value": [
opa_1         |                                                                                               [
opa_1         |                                                                                                 {
opa_1         |                                                                                                   "type": "string",
opa_1         |                                                                                                   "value": "StructValue"
opa_1         |                                                                                                 },
opa_1         |                                                                                                 {
opa_1         |                                                                                                   "type": "object",
opa_1         |                                                                                                   "value": [
opa_1         |                                                                                                     [
opa_1         |                                                                                                       {
opa_1         |                                                                                                         "type": "string",
opa_1         |                                                                                                         "value": "fields"
opa_1         |                                                                                                       },
opa_1         |                                                                                                       {
opa_1         |                                                                                                         "type": "object",
opa_1         |                                                                                                         "value": [
opa_1         |                                                                                                           [
opa_1         |                                                                                                             {
opa_1         |                                                                                                               "type": "string",
opa_1         |                                                                                                               "value": "roles"
opa_1         |                                                                                                             },
opa_1         |                                                                                                             {
opa_1         |                                                                                                               "type": "object",
opa_1         |                                                                                                               "value": [
opa_1         |                                                                                                                 [
opa_1         |                                                                                                                   {
opa_1         |                                                                                                                     "type": "string",
opa_1         |                                                                                                                     "value": "Kind"
opa_1         |                                                                                                                   },
opa_1         |                                                                                                                   {
opa_1         |                                                                                                                     "type": "object",
opa_1         |                                                                                                                     "value": [
opa_1         |                                                                                                                       [
opa_1         |                                                                                                                         {
opa_1         |                                                                                                                           "type": "string",
opa_1         |                                                                                                                           "value": "ListValue"
opa_1         |                                                                                                                         },
opa_1         |                                                                                                                         {
opa_1         |                                                                                                                           "type": "object",
opa_1         |                                                                                                                           "value": [
opa_1         |                                                                                                                             [
opa_1         |                                                                                                                               {
opa_1         |                                                                                                                                 "type": "string",
opa_1         |                                                                                                                                 "value": "values"
opa_1         |                                                                                                                               },
opa_1         |                                                                                                                               {
opa_1         |                                                                                                                                 "type": "array",
opa_1         |                                                                                                                                 "value": [
opa_1         |                                                                                                                                   {
opa_1         |                                                                                                                                     "type": "object",
opa_1         |                                                                                                                                     "value": [
opa_1         |                                                                                                                                       [
opa_1         |                                                                                                                                         {
opa_1         |                                                                                                                                           "type": "string",
opa_1         |                                                                                                                                           "value": "Kind"
opa_1         |                                                                                                                                         },
opa_1         |                                                                                                                                         {
opa_1         |                                                                                                                                           "type": "object",
opa_1         |                                                                                                                                           "value": [
opa_1         |                                                                                                                                             [
opa_1         |                                                                                                                                               {
opa_1         |                                                                                                                                                 "type": "string",
opa_1         |                                                                                                                                                 "value": "StringValue"
opa_1         |                                                                                                                                               },
opa_1         |                                                                                                                                               {
opa_1         |                                                                                                                                                 "type": "string",
opa_1         |                                                                                                                                                 "value": "users"
opa_1         |                                                                                                                                               }
opa_1         |                                                                                                                                             ]
opa_1         |                                                                                                                                           ]
opa_1         |                                                                                                                                         }
opa_1         |                                                                                                                                       ]
opa_1         |                                                                                                                                     ]
opa_1         |                                                                                                                                   }
opa_1         |                                                                                                                                 ]
opa_1         |                                                                                                                               }
opa_1         |                                                                                                                             ]
opa_1         |                                                                                                                           ]
opa_1         |                                                                                                                         }
opa_1         |                                                                                                                       ]
opa_1         |                                                                                                                     ]
opa_1         |                                                                                                                   }
opa_1         |                                                                                                                 ]
opa_1         |                                                                                                               ]
opa_1         |                                                                                                             }
opa_1         |                                                                                                           ]
opa_1         |                                                                                                         ]
opa_1         |                                                                                                       }
opa_1         |                                                                                                     ]
opa_1         |                                                                                                   ]
opa_1         |                                                                                                 }
opa_1         |                                                                                               ]
opa_1         |                                                                                             ]
opa_1         |                                                                                           }
opa_1         |                                                                                         ]
opa_1         |                                                                                       ]
opa_1         |                                                                                     }
opa_1         |                                                                                   ],

Where do you put that json structure again? Can you paste out your setup here (config, etc)?

Envoy Metadata is a protobuf::Struct. So the a protobuf;:Struct is converted to JSON, it will look like that.

Well, JWT token is also forwarded to OPA by ext_authz, you can safely assume it has been verified by Envoy. You just need to get its payload by splitting it and base64 decode the payload.

@dio : This is my complete config:

static_resources:
  listeners:
    - address:
        socket_address: { address: 0.0.0.0, port_value: 8081 }
      # use_original_dst: true
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
                codec_type: auto
                stat_prefix: ingress_http
                access_log:
                  - name: envoy.file_access_log
                    typed_config:
                      "@type": type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog
                      path: /dev/stdout
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: service
                http_filters:
                  - name: envoy.filters.http.jwt_authn
                    config:
                      providers:
                        keycloak_stage:
                          issuer: https://keycloak.REDACTED/auth/realms/internal
                          forward: true
                          payload_in_metadata: "jwt_payload"
                          local_jwks:
                            filename: /etc/envoy/keycloak_stage_internal.jwks
                      rules:
                        - match:
                            prefix: /
                          requires:
                            provider_name: keycloak_stage
                  - name: envoy.ext_authz
                    typed_config:
                      "@type": type.googleapis.com/envoy.config.filter.http.ext_authz.v2.ExtAuthz
                      failure_mode_allow: false
                      metadata_context_namespaces:
                        - envoy.filters.http.jwt_authn
                      with_request_body:
                        max_request_bytes: 100000000
                        allow_partial_message: false
                      grpc_service:
                        google_grpc:
                          target_uri: opa:9191
                          stat_prefix: ext_authz
                  - name: envoy.router
                    typed_config: {}

  clusters:
    - name: keycloak_jwks_cluster
      type: strict_dns
      connect_timeout: 5s
      load_assignment:
        cluster_name: keycloak_jwks_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: keycloak.REDACTED
                      port_value: 443
    - name: service
      connect_timeout: 0.25s
      type: strict_dns
      lb_policy: round_robin
      load_assignment:
        cluster_name: service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: docker.for.mac.localhost
                      port_value: 8080

admin:
  access_log_path: "/dev/stdout"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

@qiwzhang : The current JSON representation of the data is not very helpful. The rest of the data that is transmitted to the Policy Agent looks like one would expect a JSON structure to look like e.g.:

opa_1         |       "request": {
opa_1         |         "http": {
opa_1         |           "headers": {
opa_1         |             ":authority": "envoy:8081",
opa_1         |             ":method": "GET",
opa_1         |             ":path": "/favicon.ico",
opa_1         |             "accept": "image/webp,*/*",
opa_1         |             "accept-encoding": "gzip",
...

Regarding your second comment: I concur that verification of the JWT should be only done in envoy and/or a Keycloak Gatekeeper before that. My concern is really only how easy it is to write unit tests for the policies of the external Policy Agent. Having to to encode the requestors username into a JWT structure and then base64 encode it makes it a bit harder. If it would just be a normal part of the input JSON like the request headers are, then you could have a bunch of plain JSON files in a folder and fire them as input against the policy agent to check which decision they would produce.

@lathspell How about using "payload_in_header", if you set that, jwt_authn will add a http header with value as the jwt payload (which is base64url encoded), ext_authz will send it to you, you only need to base64 decode it.

My envoy (1.13.1) isn't forwarding anything in ext_authz filter to the external HTTP service. The docs don't specify exactly how the jwt payload will be forwarded when the following config is used:

metadata_context_namespaces:
  - envoy.filters.http.jwt_authn

(yes, i have the corresponding jwt_authn config option payload_in_metadata: client_data). what http verb is used when calling the ext_authz http server? how will the jwt payload be handed to the ext_authz http server?

The only thing that is currently forwarding any JWT information for me is to set forward: true in the jwt_authn filter. Then I see the http Authorization header being forwarded to the ext_authz http server.

Here's a full example that @shanesoh and I have been working on https://github.com/shanesoh/envoy-opa-compose

@lathspell can we close this? Seems like @qiwzhang's recommendation to put it on the header could be a solution. I'm closing this. Please let me know if you want to see a different approach to this. Thank you!

@jpermar sounds like a doc issue. Could you submit a new issue/PR for that? Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vpiduri picture vpiduri  路  3Comments

ghost picture ghost  路  3Comments

phlax picture phlax  路  3Comments

dstrelau picture dstrelau  路  3Comments

hzxuzhonghu picture hzxuzhonghu  路  3Comments