Skip to main content

Identity Verification (from your app)

This article describes how to retrieve a user's identity from a Pomerium-managed application. Pomerium uses JSON web tokens (JWT) to attest that a given request was handled by Pomerium's authorization service.

Prerequisites

To secure your app with signed headers, you'll need the following:

  • An application you want users to connect to.
  • A JWT library with support for the ES256 signing algorithm.

JWT data

When the Pass Identity Headers route option is enabled, the user's associated identity information will be included in a signed attestation JWT. This JWT is added to each upstream request in the header X-Pomerium-Jwt-Assertion. The signed attestation JWT is also available at the special /.pomerium/jwt endpoint of any URL handled by Pomerium.

The JWT will contain at least the following claims:

claimdescription
expExpiration time in seconds since the UNIX epoch.
iatIssued-at time in seconds since the UNIX epoch.
audThe domain for the upstream application (e.g. httpbin.corp.example.com).
issSame as the aud claim.
subThe user's ID, as specified by the identity provider.
emailThe user's email address.
groupsThe user's group memberships (if supported for the identity provider).
nameThe user's full name, as specified by the identity provider.
Audience and issuer claims

The audience (aud) claim defines what application the JWT is intended for. Pomerium sets the audience claim to be the domain of the target upstream application.

Since version 0.22, Pomerium sets the issuer (iss) claim also to the domain of the target upstream application. (In previous versions, this was instead set to the authentication service domain.)

Upstream services should verify that these claims match the expected domain in order to prevent token reuse between different upstream services.

If your identity provider (IdP) provides other claims 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.

JWT verification

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.

Signed headers are used to establish an extra layer of authentication. For more information on the benefits, check out our blog post on this topic!

The attestation JWT's signature can be verified using the public key retrieved from Pomerium's /.well-known/pomerium/jwks.json endpoint (on any route domain). For example:

curl https://your-app.corp.example.com/.well-known/pomerium/jwks.json | jq
{
"keys": [
{
"use": "sig",
"kty": "EC",
"kid": "ccc5bc9d835ff3c8f7075ed4a7510159cf440fd7bf7b517b5caeb1fa419ee6a1",
"crv": "P-256",
"alg": "ES256",
"x": "QCN7adG2AmIK3UdHJvVJkldsUc6XeBRz83Z4rXX8Va4",
"y": "PI95b-ary66nrvA55TpaiWADq8b3O1CYIbvjqIHpXCY"
}
]
}

(This endpoint can also be used to integrate with other systems, such as Istio. For example, see the Istio guide on Authentication Policy, and specifically the jwksUri key on the jwtRules mapping.)

caution

In order to use the /.well-known/pomerium/jwks.json endpoint you must set either the Signing Key or Signing Key File configuration option.

After verifying the JWT signature, your application should verify that JWT has not expired, by comparing the current time with the timestamps in the exp and iat claims. We recommend allowing up to 1 minute leeway in this comparison, to account for clock skew between Pomerium and your application.

And finally, your application should verify that the aud and iss claim both match the domain used to serve your application.

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 Single-Page Application

A single-page javascript application can verify the JWT using the JavaScript SDK. For example:

import { useEffect, useState } from 'react';
import { PomeriumVerifier, signOut } from '@pomerium/js-sdk';

function App() {

const [jwt, setJwt ] = useState('');

useEffect(() => {
const jwtVerifier = new PomeriumVerifier({
issuer: 'react.localhost.pomerium.io',
audience: 'react.localhost.pomerium.io',
expirationBuffer: 1000
});
jwtVerifier.verifyBrowserUser()
.then(r => setJwt(r))
.catch(e => console.log(e));
}, [])

return (
<div style={{margin: '20px'}}>
<pre>{JSON.stringify(jwt, null, 2)}</pre>
<div style={{marginTop: '20px'}}>
<button onClick={() => signOut('https://www.pomerium.io')} type="button">Sign Out Test</button>
</div>
</div>
);
}

export default App;

See the JavaScript SDK guide for more information.

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=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZzBQdk1VeVZGeWxTbGZ3eDgKSDBxMUVyOHZlOXBnY3ZzNkV6ZnR5OHlxNnFLaFJBTkNBQVM5ZC96TC9aSXd5ZGQ1RXZMb0xGMytHblVIUS9wdQpQaU45NDV1Y1RpTFRqMDhZalo3U0NJV2JHc2tiK0RIMzJ2aUc2KzRnb0FvWlFUM1R6b2kzRVl6OAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
  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 basically just a compressed, json blob as described above. Navigate to jwt.io which provides a helpful GUI 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 past 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.

caution

In an actual client, you'll want to ensure that all the other claims values are valid (like expiration, issuer, audience and so on) in the context of your application. You'll also want to make sure you have a safe and reliable mechanism for distributing the public signing key to client apps (typically, a key management service).