JWT Authentication

This HTTP filter can be used to verify JSON Web Token (JWT). It will verify its signature, audiences and issuer. It will also check its time restrictions, such as expiration and nbf (not before) time. If the JWT verification fails, its request will be rejected. If the JWT verification succeeds, its payload can be forwarded to the upstream for further authorization if desired.

JWKS is needed to verify JWT signatures. They can be specified in the filter config or can be fetched remotely from a JWKS server.

Following are supported JWT alg:

ES256, ES384, ES512,
HS256, HS384, HS512,
RS256, RS384, RS512,
PS256, PS384, PS512,
EdDSA

Configuration

  • This filter should be configured with the type URL type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication.

  • v3 API reference

This HTTP filter config has two fields:

  • Field providers specifies how a JWT should be verified, such as where to extract the token, where to fetch the public key (JWKS) and where to output its payload.

  • Field rules specifies matching rules and their requirements. If a request matches a rule, its requirement applies. The requirement specifies which JWT providers should be used.

JwtProvider

JwtProvider specifies how a JWT should be verified. It has the following fields:

  • issuer: the principal that issued the JWT, usually a URL or an email address.

  • audiences: a list of JWT audiences allowed to access. A JWT containing any of these audiences will be accepted. If not specified, the audiences in JWT will not be checked.

  • local_jwks: fetch JWKS in local data source, either in a local file or embedded in the inline string.

  • remote_jwks: fetch JWKS from a remote HTTP server, also specify cache duration.

  • forward: if true, JWT will be forwarded to the upstream.

  • from_headers: extract JWT from HTTP headers.

  • from_params: extract JWT from query parameters.

  • from_cookies: extract JWT from HTTP request cookies.

  • forward_payload_header: forward the JWT payload in the specified HTTP header.

  • claim_to_headers: copy JWT claim to HTTP header.

  • jwt_cache_config: Enables JWT cache, its size can be specified by jwt_cache_size. Only valid JWTs are cached.

Default Extract Location

If from_headers and from_params is empty, the default location to extract JWT is from HTTP header:

Authorization: Bearer <token>

and query parameter key access_token as:

/path?access_token=<JWT>

If a request has two tokens, one from the header and the other from the query parameter, all of them must be valid.

The providers field is a map, to map provider_name to a JwtProvider. The provider_name must be unique, it is referred by the fields provider_name and provider_name.

Important

If remote_jwks is used, a jwks_cluster cluster is required to be specified in the field cluster.

Due to above requirement, OpenID Connect Discovery is not supported since the URL to fetch JWKS is in the response of the discovery. It is not easy to setup a cluster config for a dynamic URL.

Token Extraction from Custom HTTP Headers

If the JWT needs to be extracted in other HTTP header, use from_headers to specify the header name. In addition to the name field, which specifies the HTTP header name, the section can specify an optional value_prefix value, as in:

41                  from_headers:
42                  - name: x-jwt-header
43                    value_prefix: jwt_value

The above will cause the jwt_authn filter to look for the JWT in the x-jwt-header header, following the tag jwt_value. Any non-JWT characters (i.e., anything other than alphanumerics, _, -, and .) will be skipped, and all following, contiguous, JWT-legal chars will be taken as the JWT.

This means all of the following will return a JWT of eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk:

x-jwt-header: jwt_value=eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk

x-jwt-header: {"jwt_value": "eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk"}

x-jwt-header: beta:true,jwt_value:"eyJFbnZveSI6ICJyb2NrcyJ9.e30.c2lnbmVk",trace=1234

The header name may be Authorization.

The value_prefix must match exactly, i.e., case-sensitively. If the value_prefix is not found, the header is skipped, not considered as a source for a JWT.

If there are no JWT-legal characters after the value_prefix, the entire string after it is taken to be the JWT. This is unlikely to succeed; the error will reported by the JWT parser.

Remote JWKS config example

38              providers:
39                provider_name1:
40                  issuer: https://example.com
41                  audiences:
42                  - bookstore_android.apps.googleusercontent.com
43                  - bookstore_web.apps.googleusercontent.com
44                  remote_jwks:
45                    http_uri:
46                      uri: https://example.com/jwks.json
47                      cluster: example_jwks_cluster
48                      timeout: 1s
49                    cache_duration: 300s

Above example fetches JWKS from a remote server with URL https://example.com/jwks.json. The token will be extracted from the default extract locations. The token will not be forwarded to upstream. JWT payload will not be added to the request header.

Following cluster example_jwks_cluster is needed to fetch JWKS.

54  clusters:
55  - name: example_jwks_cluster
56    type: STRICT_DNS
57    lb_policy: ROUND_ROBIN
58    load_assignment:
59      cluster_name: example_jwks_cluster
60      endpoints:
61      - lb_endpoints:
62        - endpoint:
63            address:
64              socket_address:
65                address: example_jwks_cluster.example.com
66                port_value: 443
67    transport_socket:
68      name: envoy.transport_sockets.tls
69      typed_config:
70        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
71        sni: example_jwks_cluster.example.com

Inline JWKS config example

Another config example using inline JWKS:

32              providers:
33                provider_name2:
34                  issuer: https://example2.com
35                  local_jwks:
36                    inline_string: >-
37                      {"keys": [
38                        {"kty":"RSA",
39                         "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z
40                              wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc
41                              5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8K
42                              JZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh
43                              6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKn
44                              qDKgw",
45                         "e":"AQAB",
46                         "alg":"RS256",
47                         "kid":"2011-04-29"}]}
48                  from_headers:
49                  - name: jwt-assertion
50                  forward: true
51                  forward_payload_header: x-jwt-payload

Above example uses config inline string to specify JWKS. The JWT will be extracted from HTTP headers as:

jwt-assertion: <JWT>

JWT payload will be added to the request header as following format:

x-jwt-payload: base64url_encoded(jwt_payload_in_JSON)

RequirementRule

RequirementRule has two fields:

  • Field match specifies how a request can be matched; e.g. by HTTP headers, or by query parameters, or by path prefixes.

  • Field requires specifies the JWT requirement, e.g. which provider is required.

Important

  • If a request matches multiple rules, the first matched rule will apply.

  • If the matched rule has empty requires field, JWT verification is not required.

  • If a request doesn’t match any rules, JWT verification is not required.

Single requirement config example

32              providers:
33                jwt_provider1:
34                  issuer: https://example.com
35                  audiences:
36                  - audience1
37                  local_jwks:
38                    inline_string: >-
39                      {"keys": [
40                        {"kty":"RSA",
41                         "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z
42                              wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc
43                              5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8K
44                              JZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh
45                              6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKn
46                              qDKgw",
47                         "e":"AQAB",
48                         "alg":"RS256",
49                         "kid":"2011-04-29"}]}
50              rules:
51              - match:
52                  prefix: /health
53              - match:
54                  prefix: /api
55                requires:
56                  provider_and_audiences:
57                    provider_name: jwt_provider1
58                    audiences:
59                    - api_audience
60              - match:
61                  prefix: /
62                requires:
63                  provider_name: jwt_provider1

Above config uses single requirement rule, each rule may have either an empty requirement or a single requirement with one provider name.

Group requirement config example

32              providers:
33                provider1:
34                  issuer: https://provider1.com
35                  local_jwks:
36                    inline_string: >-
37                      {"keys": [
38                        {"kty":"RSA",
39                         "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z
40                              wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc
41                              5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8K
42                              JZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh
43                              6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKn
44                              qDKgw",
45                         "e":"AQAB",
46                         "alg":"RS256",
47                         "kid":"2011-04-29"}]}
48                provider2:
49                  issuer: https://provider2.com
50                  local_jwks:
51                    inline_string: >-
52                      {"keys": [
53                        {"kty":"RSA",
54                         "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z
55                              wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc
56                              5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8K
57                              JZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh
58                              6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKn
59                              qDKgw",
60                         "e":"AQAB",
61                         "alg":"RS256",
62                         "kid":"2011-04-29"}]}
63              rules:
64              - match:
65                  prefix: /any
66                requires:
67                  requires_any:
68                    requirements:
69                    - provider_name: provider1
70                    - provider_name: provider2
71              - match:
72                  prefix: /all
73                requires:
74                  requires_all:
75                    requirements:
76                    - provider_name: provider1
77                    - provider_name: provider2

Above config uses more complex group requirements:

  • The first rule specifies requires_any; if any of provider1 or provider2 requirement is satisfied, the request is OK to proceed.

  • The second rule specifies requires_all; only if both provider1 and provider2 requirements are satisfied, the request is OK to proceed.

Copy validated JWT claims to HTTP request headers example

If a JWT is valid, you can add some of its claims of type (string, integer, boolean) to a new HTTP header to pass to the upstream. You can specify claims and headers in claim_to_headers field. Nested claims are also supported.

The field claim_to_headers is a repeat of message JWTClaimToHeader which has two fields:

  • Field header_name specifies the name of new http header reserved for jwt claim. If this header is already present with some other value then it will be replaced with the claim value. If the claim value doesn’t exist then this header wouldn’t be available for any other value.

  • Field claim_name specifies the claim from the verified JWT.

32              providers:
33                provider_name2:
34                  issuer: https://example2.com
35                  claim_to_headers:
36                  - header_name: x-jwt-claim-sub
37                    claim_name: sub
38                  - header_name: x-jwt-claim-nested-key
39                    claim_name: nested.claim.key
40                  - header_name: x-jwt-tenants
41                    claim_name: tenants

In this example the tenants claim is an object, therefore the JWT claim (“sub”, “nested.claim.key” and “tenants”) will be added to HTTP headers as following format:

x-jwt-claim-sub: <JWT Claim>
x-jwt-claim-nested-key: <JWT Claim>
x-jwt-tenants: <Base64 encoded JSON JWT Claim>