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:
claim | description |
---|---|
exp | Expiration time in seconds since the UNIX epoch. |
iat | Issued-at time in seconds since the UNIX epoch. |
aud | The domain for the upstream application (e.g. 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. |
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:
- 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.
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.)
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;
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=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZzBQdk1VeVZGeWxTbGZ3eDgKSDBxMUVyOHZlOXBnY3ZzNkV6ZnR5OHlxNnFLaFJBTkNBQVM5ZC96TC9aSXd5ZGQ1RXZMb0xGMytHblVIUS9wdQpQaU45NDV1Y1RpTFRqMDhZalo3U0NJV2JHc2tiK0RIMzJ2aUc2KzRnb0FvWlFUM1R6b2kzRVl6OAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
-
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 basically just a compressed, json blob as described above. Navigate to jwt.io which provides a helpful GUI 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 past 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.
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).