Setting request headers with values from a JWT

Before Istio 1.5 with the mixer it was easy to set headers related to values included in a JWT.

How can I do this in Istio 1.5?

Example:

  • Bearer token in a request includes the user id as [sub]
  • UserId should be included as header “X-user-id”
2 Likes

Do you want to inject request headers before JWT is forwarded to the application? One way you can do is to inject an EnvoyFilter after Istio authentication filter, and add your logic of settings headers there.

I found this yesterday looking for something else

1 Like

Hello,

This code works with istio 1.5.x.

Jwt auth and envoy filter

This works for me.

Envoy jwt auth adds the claims to the dynamic metadata. ISTIO by default uses the issuer as the key in the dynamic metadata.

step 1:
Update the access log so that you can see what values you get in the dynamic metadata.
add this to the access logs: “auth_jwt”:"%DYNAMIC_METADATA(envoy.filters.http.jwt_authn)%"

In this step you should be able to find out the key for which you can access your claims.

step2:
Create an envoy filter that uses the dynamic metadata to add headers to the request.

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: jwt-to-header-filter
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      app: istio-ingressgateway
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
            subFilter:
              name: "envoy.router"
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.lua
        config: 
          inlineCode: |
            function envoy_on_request(request_handle)
              local meta = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")
              local claims = meta["{add the key here from step1. This is most likely your jwt issuer url.}"]
              local user = claims.iss.."/"..claims.username
              request_handle:logInfo("username"..user)
              request_handle:headers():add("x-jwt-user", user)
            end
2 Likes

You can use outputPayloadToHeader to have istio base64 encode the jwt payload and pass it along as a a header

1 Like

Wow!! This seems the best option!

apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
  name: "jwt-echoserver"
  namespace: foo
spec:
  selector:
    matchLabels:
      app: echoserver
  jwtRules:
  - issuer: "testing@secure.istio.io"
    jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.7/security/tools/jwt/samples/jwks.json"
    outputPayloadToHeader: x-jwt

Thank you.

@inaiat How does this helps?
its indeed forward the JWT token as an header but it does not decode it on the Envoy part.

@Thet_Ko how shouldnt your EnvoyFilter includes also section for the jwt_authn filter?

10x

@chen I receive a payload in base64. Like this:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}

So, my app can handle this jwt payload.

Ho, yes my app also, but i thought the main idea for this thread was to be able to decrypt the jwt on the Envoy and use specific claims as request headers.
i fail to get the decryption part properly.

Sure… I cant handle this with latest istio (1.7.x) versions. So I use this approach (I think this easiest way)

Hi Chen,

You can add just add request authentication similar to what inaiat has done. The envoy filter will use this jwt token to do its parsing and processing.

Note this line inside the envoy metafilter:
local claims = meta["{add the key here from step1. This is most likely your jwt issuer url.}"]

Thanks, i’ve already got this working.

my problem was that i was trying to use the ‘outputPayLoadToHeader’ on the EnvoyFilter and basically do the decryption myself and that was the wrong direction.

i’ve removed the outputPayloadToHeader from the requestAuthentication as start decoding it myself on the lua script was waste of time.

the magic was indeed on the jwt_authn filter.
meta=request_handle:streamInfo():dynamicMetadata():get(“envoy.filters.http.jwt_authn”)
it did the decryption part and i only needed to read the claims from the metadata based on my issuer.

thanks:)

1 Like

Hey guys, great discussion; i’ve been scouring documentation and google searching for hours trying to find out to do exactly this. I am unfortunately not able to get it working: I am using this example:

function envoy_on_request(request_handle)
              local meta = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")
              local claims = meta["{add the key here from step1. This is most likely your jwt issuer url.}"]
              local user = claims.iss.."/"..claims.client_id
              request_handle:logInfo("username"..user)
              request_handle:headers():add("x-jwt-user", user)
            end

Will this not take a header of “Authorization: Bearer XXXXXXX”, decode the XXXX part, take out the client_id, and pass it on as a value on the header of x-jwt-user? This is my payload decoded:

{
  "scope": [],
  "client_id": "jasonselftest-Test-999999",
  "iss": "https://xxxxxxxxxxx",
  "aud": "https://xxxxx.xxxx.xxxxx.xxxxx/",
  "exp": 1610507389
}

What I am really aiming for is I want to take the “client_id” from my jwt token and store it as a metric which i can then show in Grafana; this way, our developers could see exactly what client_id is hitting them on their dashboards. Any tips or ideas? Any help would be HUGELY appreciated, this would be a big win for us. Thanks!

@chen @Thet_Ko

HI @jbilliau-rcd ,

in short it should work, but can you change it to the following:

function envoy_on_request(request_handle)
              local meta = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")
              print(meta) //do you see an object here or just nill?
              local claims = metadata["{add the key here from step1. This is most likely your jwt issuer url.}"]
              print(claims) //do you see an object here or just nill?
              local user = claims.iss.."/"..claims.client_id
              request_handle:logInfo("username"..user)
              request_handle:headers():add("x-jwt-user", user)
            end

also, are you sure you are using the right metadata name?
if you are using JWT, it should be the full url of the issuer.
the prints i’ve added will help you understand if you were able to fetch the right content.

also, i didnt understand from your message if you do get antyhing on the headers or not.

Chen

@chen I got it working! Did this:

patch:
      operation: INSERT_BEFORE
      filterClass: STATS
      value:
        name: envoy.lua
        typed_config:
          "@type": "type.googleapis.com/envoy.config.filter.http.lua.v2.Lua"
          inlineCode: |
            function envoy_on_request(request_handle)
              local meta = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")
              local claims = meta["https://api.test.xxxx.xxxx.zone"]
              local user = claims.client_id
              request_handle:logInfo("x-jwt-user"..user)
              request_handle:headers():add("x-jwt-user", user)
              end

I now see the x-jwt-user as a header. I then used this to write it to envoy access logs:

- applyTo: NETWORK_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
          access_log:
          - name: envoy.file_access_log
            typed_config:
              "@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog"
              path: /dev/stdout
              format: "%REQ(x-jwt-user)%"

Lastly, I edited my stats-1.7 envoyfilter and did this to inject it as a prom metric:

- applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        filterChain:
          filter:
            name: envoy.http_connection_manager
            subFilter:
              name: envoy.router
      proxy:
        proxyVersion: ^1\.7.*
    patch:
      operation: INSERT_BEFORE
      value:
        name: istio.stats
        typed_config:
          '@type': type.googleapis.com/udpa.type.v1.TypedStruct
          type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
          value:
            config:
              configuration:
                '@type': type.googleapis.com/google.protobuf.StringValue
                value: |
                  {"debug":false,"metrics":[{"dimensions":{"client_id":"string(request.headers[\"x-jwt-user\"])"}}],"stat_prefix":"istio"}
              root_id: stats_inbound
              vm_config:
                code:
                  local:
                    inline_string: envoy.wasm.stats
                runtime: envoy.wasm.runtime.null
                vm_id: stats_inbound

Now, I can display istio_request_total by client_id and response code, which is pretty awesome. Thanks for all the examples, this was a very helpful thread.

Cool glad it works:)

Can you please share your envoy filter? I am stuck on this for so long.

you have the envoy filter on this thread. where are you stuck? share what you did and will try to help.

I am getting nil value in dynamicMetadata()