Zero Fundamentals: Single Sign-on with JWTs
This guide shows you how to set up Pomerium to achieve single sign-on (SSO). You'll configure Pomerium to forward JWTs as identity headers to an upstream service called Grafana. You'll configure Grafana to support JWT authentication.
Watch the video for a quick walkthrough or read the guide below for step-by-step instructions.
Make sure you've completed the following guides:
Each guide builds on the same configuration files. In this guide, you'll add JWT settings to your Grafana instance.
What is a JWT?
If you’re unfamiliar with JWTs, here’s a quick definition sourced from JWT.io:
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
The claims within a JWT represent data encoded into a JSON object, known as the Payload. Because JWTs can be cryptographically signed and verified with public and private key pairs, they are often used for authentication and authorization in applications.
When JWTs are used for authentication and authorization, they often contain claims with identifying user data, like the user's email address, full name, and username. However, claims also contain other standard data, known as Registered Claim Names.
For example:
{
"aud": "verify.trusted-dog-1049.pomerium.app",
"email": "user@example.com",
"exp": 1713474536,
"groups": [],
"iat": 1713474228,
"iss": "verify.trusted-dog-1049.pomerium.app",
"jti": "9d1da135-223a-4f6c-8524-cfb1e4fe0452",
"name": "John Doe",
"sid": "9d1da135-223a-4f6c-8524-cfb1e4fe0452",
"sub": "9c2de20b-7b02-4e48-9f12-2e4a3d57bd8c",
"user": "9c2de20b-7b02-4e48-9f12-2e4a3d57bd8c"
}
How SSO works with Pomerium
Providing identity verification on the application level as a core capability typically falls outside of the realm of support for most reverse proxies.
With Pomerium, the ability to mint, sign, and forward a JWT as an HTTP request header is native to the product – and, it's as simple as making one configuration change in the Zero Console.
Now, let's see how SSO with JWTs works with Pomerium:
-
End-user accesses the upstream service
-
Pomerium authenticates the end-user against an identity provider and creates a Pomerium JWT
-
Pomerium authorizes the user, then signs and forwards the JWT Assertion Header to the upstream service
-
Upstream service fetches the public key from the JWKS endpoint, validates the signature, and grants the end-user access
In this way, the upstream application is secured on the protocol layer with an encrypted TLS connection, and on the application layer with JWT authentication. The user is also subject to the rules of the authorization policy.
Now, let's configure Pomerium to forward the JWT.
Configure SSO
Apply Pass Identity Headers
In Routes:
-
Select your Grafana route
-
Select the Headers tab
-
Apply the Pass Identity Headers setting
The Pass Identity Headers setting instructs Pomerium to forward the signed x-pomerium-jwt-assertion
HTTP header to the upstream application.
Add Grafana JWT settings
In your Docker Compose file, add the following environment variables to your Grafana service:
grafana:
image: grafana/grafana:latest
ports:
- 3000:3000
networks:
main: {}
environment:
- GF_AUTH_SIGNOUT_REDIRECT_URL=https://grafana.<CLUSTER_SUBDOMAIN>.pomerium.app/.pomerium/sign_out
- GF_AUTH_JWT_ENABLED=true
- GF_AUTH_JWT_HEADER_NAME=X-Pomerium-Jwt-Assertion
- GF_AUTH_JWT_EMAIL_CLAIM=email
- GF_AUTH_JWT_USERNAME_CLAIM=sub
- GF_AUTH_JWT_JWK_SET_URL=https://authenticate.<CLUSTER_SUBDOMAIN>.pomerium.app/.well-known/pomerium/jwks.json
- GF_AUTH_JWT_CACHE_TTL=60m
- GF_AUTH_JWT_AUTO_SIGN_UP=true
volumes:
- ./grafana-storage:/var/lib/grafana
See Grafana's Configure JWT authentication docs for more information about these settings.
In lines 8
and 13
, replace CLUSTER_SUBDOMAIN
with your cluster subdomain.
For example, if your starter domain is curious-cat-9999.pomerium.app
, your cluster subdomain would be curious-cat-9999
.
You also need to add another alias for the Pomerium Authenticate service:
pomerium:
image: pomerium/pomerium:v0.27.2
ports:
- 443:443
restart: always
environment:
POMERIUM_ZERO_TOKEN: <CLUSTER_TOKEN>
XDG_CACHE_HOME: /var/cache
volumes:
- pomerium-cache:/var/cache
networks:
main:
aliases:
- verify.<CLUSTER_SUBDOMAIN>.pomerium.app
- authenticate.<CLUSTER_SUBDOMAIN>.pomerium.app
Adding the alias to your pomerium
service makes it possible for the Grafana and Pomerium containers to communicate over the Docker network. Without it, Grafana won't be able to reach the public JWKS endpoint.
Test the connection
Now, run your containers:
docker compose up
Access your Grafana route. You'll notice it signs you in without prompting you to enter a username or password.
Read the following documentation to learn more about identity verification in Pomerium:
We also provide SDKs for JWT verification in the following languages:
Up Next: Build Advanced Policies
Great job! You successfully configured SSO in Pomerium. Next, you'll learn how to build more advanced policies.
Go to Build Advanced Policies.
Configuration file state
At this point, your Docker Compose file should look like this:
services:
pomerium:
image: cr.pomerium.com/pomerium/pomerium:v0.27.2
ports:
- 443:443
restart: always
environment:
POMERIUM_ZERO_TOKEN: <CLUSTER_TOKEN>
XDG_CACHE_HOME: /var/cache
volumes:
- pomerium-cache:/var/cache
networks:
main:
aliases:
- verify.<CLUSTER_SUBDOMAIN>.pomerium.app
- authenticate.<CLUSTER_SUBDOMAIN>.pomerium.app
verify:
image: cr.pomerium.com/pomerium/verify:latest
networks:
main:
aliases:
- verify
grafana:
image: grafana/grafana:latest
ports:
- 3000:3000
networks:
main: {}
environment:
- GF_AUTH_SIGNOUT_REDIRECT_URL=https://grafana.<CLUSTER_SUBDOMAIN>.pomerium.app/.pomerium/sign_out
- GF_AUTH_JWT_ENABLED=true
- GF_AUTH_JWT_HEADER_NAME=X-Pomerium-Jwt-Assertion
- GF_AUTH_JWT_EMAIL_CLAIM=email
- GF_AUTH_JWT_USERNAME_CLAIM=sub
- GF_AUTH_JWT_JWK_SET_URL=https://authenticate.<CLUSTER_SUBDOMAIN>.pomerium.app/.well-known/pomerium/jwks.json
- GF_AUTH_JWT_CACHE_TTL=60m
- GF_AUTH_JWT_AUTO_SIGN_UP=true
volumes:
- ./grafana-storage:/var/lib/grafana
networks:
main: {}
volumes:
pomerium-cache: