Skip to main content

JWT Verification

This guide provides several methods to verify and validate the Pomerium JWT forwarded in signed the X-Pomerium-Jwt-Assertion header:

JWT validation requirements

Before trusting any user identity information in the JWT, your application should verify:

  1. The JWT has a valid signature from a trusted source.
  2. The JWT has not expired.
  3. The JWT audience and issuer match your application's domain.

See JWT validation for specific instructions on validating each of these requirements.

Verification in a Go application

For an application written in Go, you can use the Go SDK to perform the necessary verification steps. For example:

package main

import (
"fmt"
"log"
"net/http"
"time"

"github.com/go-jose/go-jose/v3/jwt"
"github.com/pomerium/sdk-go"
)

func main() {
verifier, err := sdk.New(&sdk.Options{
Expected: &jwt.Expected{
// Replace the following with the domain for your service:
Issuer: "sdk-example.localhost.pomerium.io",
Audience: jwt.Audience([]string{
"sdk-example.localhost.pomerium.io"}),
},
})
if err != nil {
log.Fatalln(err)
}

http.Handle("/", sdk.AddIdentityToRequest(verifier)(handler{}))
log.Fatalln(http.ListenAndServe(":8080", nil))
}

type handler struct{}

func (handler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// Check the JWT verification result.
id, err := sdk.FromContext(req.Context())
if err != nil {
fmt.Fprintln(res, "verification error:", err)
return
}

fmt.Fprintf(res, "verified user identity (email %s)\n", id.Email)
}

Verification in a Node.js application

Pomerium's JavaScript SDK provides a server-side solution to verify JWTs issued by the authorization service.

Requirements to use the JavaScript SDK

The JavaScript SDK is available as an NPM package and can be imported using CommonJS or ECMAScript modules.

To use the JavaScript SDK, you need:

  • Node.js (version 18+)
  • NPM (to install Node.js and Yarn)
  • Yarn (preferred package manager)

The following code provides a minimum working example of how JWT verification works using the JavaScript SDK in a Node.js app:

const express = require("express");
const { PomeriumVerifier } = require('@pomerium/js-sdk');
const app = express();

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; //just for dev

app.get("/tofu", (request, response) => {
const jwtVerifier = new PomeriumVerifier({});
jwtVerifier.verifyJwt(request.get('X-Pomerium-Jwt-Assertion')).then(r => response.send(r))
});

app.get("/wrong-audience", (request, response) => {
const jwtVerifier = new PomeriumVerifier({
audience: [
'correct-audience.com'
],
expirationBuffer: 1000
});
jwtVerifier.verifyJwt(request.get('X-Pomerium-Jwt-Assertion'))
.then(r => response.send(r))
.catch(e => response.send(e.message));
});

app.get("/wrong-issuer", (request, response) => {
const jwtVerifier = new PomeriumVerifier({
issuer: 'correct-issuer.com',
expirationBuffer: 1000
});
jwtVerifier.verifyJwt(request.get('X-Pomerium-Jwt-Assertion'))
.then(r => response.send(r))
.catch(e => response.send(e.message));
});

app.get("/expired", (request, response) => {
const jwtVerifier = new PomeriumVerifier({
expirationBuffer: -10000
});
jwtVerifier.verifyJwt(request.get('X-Pomerium-Jwt-Assertion'))
.then(r => response.send(r))
.catch(e => response.send(e.message));
});

app.listen(3010, () => {
console.log("Listen on the port 3010...");
});
Trust on first use (TOFU)

The issuer and audience parameters are optional. If you don’t define them, PomeriumVerifier applies firstUse by default to the JWT provided by the identity provider. PomeriumVerifier verifies subsequent requests with these claims.

If you define the issuer and audience parameters, PomeriumVerifier verifies their values against the claims provided by the identity provider.

The issuer and audience parameters should both be set to the domain of the upstream application without the prefixed protocol (for example, httpbin.corp.example.com).

Note: We strongly recommend that you explicitly define the expected issuer and audience claims. Relying on a TOFU policy is dangerous in ephemeral serverless environments (such as AWS Lamda or Cloud Run), where applications are typically short-lived.

Manual verification

Though you will likely verify signed headers programmatically in your application's middleware with a third-party JWT library, if you are new to JWT it may be helpful to show what manual verification looks like.

  1. Provide Pomerium with a base64-encoded Elliptic Curve (NIST P-256) Private Key. In production, you'd likely want to get these from your key management service (KMS).

    openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
    openssl ec -in ec_private.pem -pubout -out ec_public.pem
    # careful! this will output your private key in terminal
    cat ec_private.pem | base64

    Copy the base64-encoded value of your private key to Pomerium's environmental configuration variable SIGNING_KEY.

    SIGNING_KEY=...
  2. Reload Pomerium. Navigate to httpbin (by default, https://httpbin.corp.${YOUR-DOMAIN}.com), and log in as usual. Click request inspection. Select /headers. Click try it out and then execute. You should see something like the following.

    httpbin displaying jwt headers

  3. X-Pomerium-Jwt-Assertion is the signature value. It's less scary than it looks, and is basically just a compressed, JSON blob as described above. Navigate to jwt.io, which provides a helpful user interface to manually verify JWT values.

  4. Paste the value of X-Pomerium-Jwt-Assertion header token into the Encoded form. You should notice that the decoded values look much more familiar.

    httpbin displaying decoded jwt

  5. Finally, we want to cryptographically verify the validity of the token. To do this, we will need the signer's public key. You can simply copy and paste the output of cat ec_public.pem.

    httpbin displaying verified jwt

Voila! Hopefully walking through a manual verification has helped give you a better feel for how signed JWT tokens are used as a secondary validation mechanism in pomerium.