JWT Authentication
This document describes how Pomerium supports JWT authentication in upstream services with JSON web tokens (JWTs).
Overview
JWTs provide a secure and efficient means to authenticate and authorize users before they can access upstream services behind Pomerium. When configured for JWT authentication, Pomerium sends its own JWT to the upstream service. By verifying the Pomerium JWT, the upstream service can:
- Confirm that the Pomerium Proxy service handled the client request before forwarding it.
- Make application-level authorization decisions based on the user's associated identity information.
Why JWT authentication?
Identity verification
JWT authentication through Pomerium enables an upstream service to verify a user's identity based on claims contained in the JWT. Pomerium signs and issues a new JWT based on the ID token received from the service's configured identity provider.
Request verification
Pomerium places the newly minted JWT in a JWT assertion header. The upstream service should only accept the incoming request if it satisfies all JWT validation conditions.
By validating the JWT, the upstream service can assert that:
- The request originated from Pomerium.
- The user was authenticated.
- The request was authorized in accordance with the route's authorization policy.
The Pomerium JWT does not contain any path information for an upstream service. If you've configured multiple routes with different paths for the same upstream service (such as an /admin
route that grants access to a limited set of users), the application can't determine which Pomerium route the JWT corresponds to.
Single Sign-on (SSO)
You can configure upstream services to accept the Pomerium JWT to achieve an SSO authentication flow. This capability is completely free and relatively easy to configure depending on the upstream service and your identity provider.
See our Grafana guide for a real-world example of how configuring both Pomerium and and an upstream service can provide easy SSO access for your end users.
JWT authentication flow
Identity provider authentication
Pomerium requires users to authenticate against an OIDC-compliant identity provider before authorizing or denying a request to an upstream service.
After successful authentication, Pomerium mints a new Pomerium JWT based on the ID token generated by the identity provider. (This is Pomerium's default behavior, even if you haven't configured Pomerium to support JWT authentication.)
JWT assertion header
Pomerium signs its JWT with a signing key. If the pass identity headers setting is enabled, Pomerium will place the JWT into a special HTTP header called the JWT assertion header. Pomerium includes the JWT assertion header in every request it forwards to the upstream service.
Pomerium passes the JWT in the X-Pomerium-Jwt-Assertion
HTTP header, and encodes it according to RFC7519.
JWT validation
The upstream service receives the X-Pomerium-Jwt-Assertion-Header
with the encrypted JWT. To validate a JWT, the service should check the following items:
JWT signature
The upstream service should validate that the JWT was signed by the issuing authority.
Pomerium issues and signs the new JWT with a private signing key. To validate the signature, the upstream service must fetch the corresponding public key from Pomerium's JSON web key set (JWKS) endpoint.
To configure an upstream service to fetch the public key:
- Get the hostname from the JWT's
iss
claim - Append the
/.well-known/pomerium/jwks.json
path to the hostname - Prepend the
https://
scheme to the URL - Set the
Accept: application/json
header in the request
For example:
- Hosted Authenticate
- Self-hosted Authenticate
curl https://service.corp.example.com/.well-known/pomerium/jwks.json \
-H 'Accept: application/json'
curl https://<AUTHENTICATE-SERVICE-URL>/.well-known/pomerium/jwks.json \
-H 'Accept: application/json'
The returned JWK key set contains Pomerium's public keys. Use the kid
claim provided in the Pomerium JWT header to identify the correct key in the returned key set.
{
"keys": [
{
"use": "sig",
"kty": "EC",
"kid": "ccc5bc9d835ff3c8f7075ed4a7510159cf440fd7bf7b517b5caeb1fa419ee6a1",
"crv": "P-256",
"alg": "ES256",
"x": "QCN7adG2AmIK3UdHJvVJkldsUc6XeBRz83Z4rXX8Va4",
"y": "PI95b-ary66nrvA55TpaiWADq8b3O1CYIbvjqIHpXCY"
}
]
}
If the JWT signature can't be validated, the JWT is invalid and can't be trusted.
Aud and iss claims
The upstream service should verify that the aud
and iss
claims match the domain used to serve your application.
The aud
claim identifies the recipient the JWT is intended for. In the context of a service behind Pomerium, the aud
claim should always be set as the upstream service's domain name.
Since v0.22, Pomerium sets the iss
claim also to the domain of the target upstream service. (In previous versions, this was instead set to the authenticate service domain.)
If the domain provided in the aud
and iss
claims doesn't match the upstream service's domain name, the JWT is invalid and can't be trusted.
{
"aud": "verify.pomerium.app",
"iss": "verify.pomerium.app"
}
JWT timestamps
The upstream service should verify that the Pomerium JWT has not expired.
The iat
claim informs you at what time the JWT was issued. The exp
claim specifies the expiration time on or after which the JWT must be considered invalid. By default, Pomerium sets the exp
claim to expire 5 minutes after the time it was issued.
By comparing the current time with the timestamps in the exp
and iat
claims, you can verify if the JWT has expired or not. We recommend allowing up to a 1-minute leeway when comparing the exp
and iat
timestamps to account for clock skew between Pomerium and the upstream service.
If the JWT has expired, it is invalid and can't be trusted.
Pomerium's JWT Verification guide shows you how to use our custom JWT libraries to parse and validate the Pomerium JWT in an upstream service.
After the upstream service validates the JWT, it can accept the request and trust other claims present in the JWT.
The Pomerium JWT
Pomerium generates a new Pomerium JWT based on the claims data contained in the original ID token. In addition to including standard claims as defined in RFC7519, Pomerium also injects its own claims into the Pomerium JWT as well. (See JWT claims data below for more details.)
The original ID token sourced from an identity provider is never modified or leaked to end users or upstream services.
Pomerium JWT claims data
When Pomerium is configured for JWT authentication with the pass identity headers setting, the user's associated identity information will be included in the JWT assertion header in each upstream request.
The Pomerium JWT contains at least the following claims:
JWT Claim | Description |
---|---|
jti | A randomly generated UUID that represents the JWT ID. |
exp | Expiration time in seconds since the UNIX epoch. Set to expire 5 minutes after iat time. |
iat | Issued-at time in seconds since the UNIX epoch. |
aud | The domain for the upstream application (for example, httpbin.corp.example.com ). |
iss | Same as the aud claim. |
sub | The user's ID, as specified by the identity provider. |
email | The user's email address. |
groups | The user's group memberships (if supported for the identity provider). |
name | The user's full name, as specified by the identity provider. |
The jti
claim (the JWT ID) contains a unique identifier assigned to each Pomerium JWT. If you can implement a system that checks the jti
value in real time, you can prevent session replay attempts. Or, if you persist the jti
value in your logs, you can detect replayed JWTs after the fact.
JWT Settings
Use these settings to configure Pomerium to forward the Pomerium JWT to upstream services:
If your identity provider provides other claims not included in the Pomerium JWT that you would like to pass to your application, you can use the JWT Claims Headers option to include them in the JWT as well.