Parsing JWT claims and send via custom header at ingress gateway

Hi

I am using istio ingressgateway 1.9.8 and using JWT token validation at istio gateway level.

However is it possible to parse the JWT claims and send to upstream service in a custom header ?

e.g. say “iss” claim as defined by request.auth.claims[iss] . Is it possible to send this in a custom header ?

One possible way can be using envoy filters but is it supported natively using istio ?

Thanks in advance !

Regards

@YangminZhu Is this something related to what you working on?

I found below proposal for the same . Please let me when this solution will be available

[JWT claim to HTTP header - Google Docs] (JWT Claims to Http Header Proposal)

Alternatively using envoy filter approach I was not able to make it work . Can someone please suggest if with istio 1.9.8 or any other latest version will envoy filter works and any other links for the same.

Below is the envoy filter i am using . It’s not able to extract claims and returns with “LUA claims” log. Please let me know if envoy filter is correct ?


apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: lua-filter
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
              subFilter:
                name: "envoy.filters.http.jwt_authn"
      patch:
        operation: INSERT_AFTER
        value:
          name: envoy.filters.http.lua
          typed_config:
            "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
            inlineCode: |
              function envoy_on_request(request)
                  local ok, message = pcall(handle_request, request)
                  if not ok then
                      request:logInfo(" pcall ")
                      request:logWarn(message)
                  end
                  request:logInfo("Exit  handle request OK")
              end
              function handle_request(request)
                  request:logInfo("Enter handle request")
                  local meta = request:streamInfo():dynamicMetadata()
                  if not meta then
                      request:logInfo("meta")
                      return
                  end
                  local jwt_filter_meta = meta:get("envoy.filters.http.jwt_authn")
                  if not jwt_filter_meta then
                      request:logInfo("jwt_filter_meta")
                      return
                  end
                  local claims = jwt_filter_meta["https://<keycloak domain>/identity/connect/auth/realms/<realm>"] # issuer
                  if not claims then
                      request:logInfo("LUA claims")
                      return
                  end
                  local iss = claims["iss"]
                  if not iss then
                      request:logInfo("iss")
                      return
                  end
                  request:headers():add("ISS", iss)
                  request:logInfo("**********************LUA Exit handle_request")
              end

Thanks.

@jnitin we did exactly this, see below for how we figured it out:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: extract-jwt-info
  namespace: istio-config
spec:
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
            subFilter:
              name: "envoy.filters.http.router"
      proxy:
        proxyVersion: ^1\.9.*
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.lua
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
          inlineCode: |
            function envoy_on_request(request_handle)
              -- getting jwt metadata
              local meta = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")
              local claim_table = {}
              -- setting ping and auth0 tokens and setting claim
              claim_table["https://sso.xxxxxx.com/"] = "https://ql.custom.openid.com/client_name"
              claim_table["https://api.xxxxx.xxxxx.xxxxx"] = "client_id"
              claim_table["https://sso.xxxxx.xxxx.com/"] = "https://ql.custom.openid.com/client_name"
              claim_table["https://sso.xxxxx.xxxxx.com/"] = "https://ql.custom.openid.com/client_name"
              claim_table["https://api.xxxx.xxxxx-np.xx.xx"] = "client_id"
              claim_table["https://api.xxxx.xxxx-np.xxx.xx"] = "client_id"
              -- searching for metadata
              for k, v in pairs(claim_table) do
                if meta then
                  if meta[k] ~= nil then
                    local claims = meta[k]
                    local jwt_client_name_value = claims[v]
                    -- setting found jwt_client_name in dynamicmetadata to be consumed by metrics/logs
                    request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "jwt_client_name", jwt_client_name_value)
                    break
                  end
                end
              end
            end

I then log it in envoy by adding this to the IstioOperator object:

accessLogFormat: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%UPSTREAM_TRANSPORT_FAILURE_REASON%\" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME% %DYNAMIC_METADATA(envoy.filters.http.lua:jwt_client_name)%\n"

Note the last field - “%DYNAMIC_METADATA(envoy.filters.http.lua:jwt_client_name)%”

Lastly, we even throw it on our istio_request_per_second metric so it logs in prometheus and we can track RPS per client id!

telemetry:
      v2:
        prometheus:
          configOverride:
            inboundSidecar:
              debug: false
              stat_prefix: istio
              metrics:
                - name: requests_total
                  dimensions:
                    jwt_client_name: string(metadata.filter_metadata["envoy.filters.http.lua"]["jwt_client_name"])

You can then require a JWT using the RequestAuthentication, and authorize it using the AuthorizationPolicy object.

  apiVersion: security.istio.io/v1beta1
  kind: RequestAuthentication
  metadata:
    name: test-whale-jwt
    namespace: test-whale
  spec:
    jwtRules:
    - audiences:
      - urn:ffffff-api:fffffff-444444:Test
      forwardOriginalToken: true
      issuer: https://sso.xxxxxx.xxxxx.com/
      jwksUri: https://sso.xxxxx.xxxxxx.com/.well-known/jwks.json

On your auth policy:

- from:
      - source:
          principals:
          - cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account
          remoteIpBlocks:
          - 12.33.148.0/24
      to:
      - operation:
          hosts:
          - test-whale.dddddd.fffff.zone
          methods:
          - '*'
          paths:
          - /api/*
      when:
      - key: request.auth.claims[aud]
        values:
        - urn:ffffff-api:fffffff-444444:Test
      - key: request.auth.claims[scope]
        values:
        - read:data