JWT Verification
This guide provides several methods to verify and validate the Pomerium JWT forwarded in signed the X-Pomerium-Jwt-Assertion
header:
- Verification in a Go application
- Verification in a single-page application
- Manual verification
JWT validation requirements
Before trusting any user identity information in the JWT, your application should verify:
- The JWT has a valid signature from a trusted source.
- The JWT has not expired.
- 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:
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.
-
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 | base64Copy the base64-encoded value of your private key to Pomerium's environmental configuration variable
SIGNING_KEY
.SIGNING_KEY=...
-
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. -
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. -
Paste the value of
X-Pomerium-Jwt-Assertion
header token into theEncoded
form. You should notice that the decoded values look much more familiar. -
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
.
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.