# JWT Verification

This example demonstrates how to verify the Pomerium JWT assertion header (opens new window) using Envoy (opens new window). This is useful for legacy or 3rd party applications which can't be modified to perform verification themselves.

# Requirements

# Overview

Two 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

Once running, the user visits verify.localhost.pomerium.io (opens new window), is authenticated through authenticate.localhost.pomerium.io (opens new window), and then the HTTP request is sent to envoy which proxies it to verify.pomerium.com (opens new window).

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 (opens new window).

# Setup

# 1. Docker Compose

Create a docker-compose.yaml file containing:

version: "3.8"
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

  envoy-jwt-checker:
    image: envoyproxy/envoy:v1.17.1
    ports:
      - "10000:10000"
    volumes:
      - type: bind
        source: ./cfg/envoy.yaml
        target: /etc/envoy/envoy.yaml

# 2. Certificates

Using mkcert (opens new window) generate a certificate for *.localhost.pomerium.io in a certs directory:

mkdir certs
cd certs
mkcert '*.localhost.pomerium.io'

# 3. Envoy Configuration

Create a cfg directory containing the following envoy.yaml file:

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: verify
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: egress-verify
                            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:
                            - verify.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-verify
      connect_timeout: 0.25s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: verify
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: verify.pomerium.com
                      port_value: 443
      transport_socket:
        name: tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          sni: verify.pomerium.com
    - 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

Envoy configuration can be quite verbose, but the crucial bit is the HTTP filter:

- 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:
          - verify.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

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 (opens new window).

# 4. Pomerium Configuration

Create a pomerium.yaml file in the cfg directory containing:

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: WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg=
shared_secret: WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg=
signing_key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUdxWllpVzJycVo3TUdKTGp4bnNZVWJJcmZxNFdwR044RlgzQVh2UnRjSHdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFYVd1UkNKMjFrL2JvUjNNRytPOVlHQjNXR0R1anVXMHFLVWhucUVwVS9JKzFoZmhuZEJ0WApDZGFpaGVGb0FOWXVCRUp3MFZhRml6QnVZb3l5RVAzOXBRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

routes:
  - from: https://verify.localhost.pomerium.io
    to: http://envoy-jwt-checker:10000
    policy:
      - allow:
          or:
            - domain:
                is: pomerium.com
    pass_identity_headers: true

You will need to replace the identity provider credentials for this to work.

# Run

You should now be able to run the example with:

docker-compose up

Visit verify.localhost.pomerium.io (opens new window), login and you see the Pomerium verify page. However, visiting Envoy directly via localhost:10000 (opens new window) should return a Jwt is missing error, thus requiring Pomerium to access Envoy.