--- title: JWT Verification lang: en-US meta: - name: keywords content: pomerium identity-access-proxy envoy jwt description: >- This example demonstrates how to verify the Pomerium JWT assertion header using Envoy. --- # JWT Verification This example demonstrates how to verify the [Pomerium JWT assertion header](https://www.pomerium.io/reference/#pass-identity-headers) using [Envoy](https://www.envoyproxy.io/). This is useful for legacy or 3rd party applications which can't be modified to perform verification themselves. This guide is a practical demonstration of some of the concept of mutual authentication, using JSON web tokens (**JWTs**). ## Requirements - [Docker](https://www.docker.com/) - [Docker Compose](https://docs.docker.com/compose/) - [mkcert](https://github.com/FiloSottile/mkcert) This guide assumes you already have a working IdP connection to provide user data. See our [Identity Provider](/docs/identity-providers/readme.md) docs for more information. ## Overview Three services are configured in a `docker-compose.yaml` file: - `pomerium` running an all-in-one deployment of Pomerium on `*.localhost.pomerium.io` - `envoy-jwt-checker` running envoy with a JWT Authn filter - `httpbin` as our example legacy application without JWT verifivation. In our Docker Compose configuration we'll define two networks. `pomerium` and `envoy-jwt-checker` will be on the `frontend` network, simulating your local area network (**LAN**). `envoy-jwt-checker` will also be on the `backend` network, along with `httpbin`. This means that `envoy-jwt-checker` is the only other service that can communicate with `httpbin`. Once running, the user visits [verify.localhost.pomerium.io], is authenticated through [authenticate.localhost.pomerium.io], and then the HTTP request is sent to envoy which proxies it to the httpbin app. Before allowing the request Envoy will verify the signed JWT assertion header using the public key defined by `authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json`. ## Setup The configuration presented here assumes a working route to the domain space `*.localhost.pomerium.io`. You can make entries in your `hosts` file for the domains used or change this value to match your local environment. ::: tip Mac and Linux users can use DNSMasq to map the `*.localhost.pomerium.io` domain (including all subdomains) to a specified test address: - [Local Development with Wildcard DNS] (macOS) - [Local Development with Wildcard DNS on Linux] ::: 1. Create a `docker-compose.yaml` file containing: ```yaml version: "3.9" networks: frontend: driver: "bridge" backend: driver: "bridge" services: pomerium: image: pomerium/pomerium:latest ports: - "443:443" volumes: - type: bind source: ./cfg/pomerium.yaml target: /pomerium/config.yaml - type: bind source: ./certs/_wildcard.localhost.pomerium.io.pem target: /pomerium/_wildcard.localhost.pomerium.io.pem - type: bind source: ./certs/_wildcard.localhost.pomerium.io-key.pem target: /pomerium/_wildcard.localhost.pomerium.io-key.pem networks: - frontend envoy-jwt-checker: image: envoyproxy/envoy:v1.17.1 ports: - "10000:10000" volumes: - type: bind source: ./cfg/envoy.yaml target: /etc/envoy/envoy.yaml networks: frontend: aliases: - "httpbin-sidecar" backend: httpbin: image: kennethreitz/httpbin ports: - "80:80" networks: - backend ``` 1. Using [`mkcert`](https://github.com/FiloSottile/mkcert), generate a certificate for `*.localhost.pomerium.io` in a `certs` directory: ```bash mkdir certs cd certs mkcert '*.localhost.pomerium.io' ``` 1. Create a `cfg` directory containing the following `envoy.yaml` file. Envoy configuration can be quite verbose, but the crucial bit is the HTTP filter (highlighted below): ```yaml{30-49} admin: access_log_path: /dev/null address: socket_address: { address: 127.0.0.1, port_value: 9901 } static_resources: listeners: - name: ingress-http address: socket_address: { address: 0.0.0.0, port_value: 10000 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: verify virtual_hosts: - name: httpbin domains: ["httpbin-sidecar"] routes: - match: prefix: "/" route: cluster: egress-httpbin auto_host_rewrite: true http_filters: - name: envoy.filters.http.jwt_authn typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication providers: pomerium: issuer: authenticate.localhost.pomerium.io audiences: - httpbin.localhost.pomerium.io from_headers: - name: X-Pomerium-Jwt-Assertion remote_jwks: http_uri: uri: https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json cluster: egress-authenticate timeout: 1s rules: - match: prefix: / requires: provider_name: pomerium - name: envoy.filters.http.router clusters: - name: egress-httpbin connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: httpbin endpoints: - lb_endpoints: - endpoint: address: socket_address: address: httpbin port_value: 80 - name: egress-authenticate connect_timeout: '0.25s' type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: authenticate endpoints: - lb_endpoints: - endpoint: address: socket_address: address: pomerium port_value: 443 transport_socket: name: tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext sni: authenticate.localhost.pomerium.io ``` This configuration pulls the JWT out of the `X-Pomerium-Jwt-Assertion` header, verifies the `iss` and `aud` claims and checks the signature via the public key defined at the `jwks.json` endpoint. Documentation for additional configuration options is available here: [Envoy JWT Authentication](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter#config-http-filters-jwt-authn). 1. Create a `pomerium.yaml` file in the `cfg` directory containing: ```yaml authenticate_service_url: https://authenticate.localhost.pomerium.io certificate_file: "/pomerium/_wildcard.localhost.pomerium.io.pem" certificate_key_file: "/pomerium/_wildcard.localhost.pomerium.io-key.pem" idp_provider: google idp_client_id: REPLACE_ME idp_client_secret: REPLACE_ME cookie_secret: REPLACE_ME shared_secret: REPLACE_ME signing_key: REPLACE_ME routes: - from: https://httpbin.localhost.pomerium.io to: http://httpbin-sidecar:10000 pass_identity_headers: true policy: - allow: or: - domain: is: example.com ``` Replace the identity provider credentials, secrets, and signing key. Adjust the policy to match your configuration. ## Run You should now be able to run the example with the following steps. 1. Turn on the example configuration in Docker: ```bash docker-compose up ``` 1. Visit [httpbin.localhost.pomerium.io](https://httpbin.localhost.pomerium.io). Login and you will be redirected to the httpbin page. 1. In this network configuration you cannot access `httpbin` directly. However, visiting Envoy directly via [localhost.pomerium.io:10000/](http://localhost.pomerium.io:10000/) will return a `Jwt is missing` error, confirming that you must authenticate with Pomerium to access Envoy, and any services accessible through it. [authenticate.localhost.pomerium.io]: https://authenticate.localhost.pomerium.io [httpbin.localhost.pomerium.io]: https://verify.localhost.pomerium.io [Local Development with Wildcard DNS on Linux]: https://sixfeetup.com/blog/local-development-with-wildcard-dns-on-linux [Local Development with Wildcard DNS]: https://blog.thesparktree.com/local-development-with-wildcard-dns [verify.localhost.pomerium.io]: https://verify.localhost.pomerium.io