Verify tokens
What's inside a Gatekeep ID token, how to check its Ed25519 signature against your JWKS, and the checks you must not skip.
Your OIDC library does all of this for you. Read on if you’re verifying tokens by hand, writing a backend that receives tokens from a frontend, or just want to know what’s going on.
Inside an ID token
A token is three base64url parts joined by dots: a header, a payload and a signature.
// header
{ "alg": "EdDSA", "typ": "JWT", "kid": "2026-10-active-1d8f12" }
// payload
{
"iss": "https://acme.gatekeepauth.com",
"sub": "01JB7Q4M9W2X8YDN3T5K6RZ0HV",
"aud": "acme-notes",
"exp": 1793318400,
"iat": 1793317500,
"email": "ada@acme.dev",
"email_verified": true,
"nonce": "n-0S6_WzA2Mj"
}
The checks
Reject the token unless all of these pass:
algis exactlyEdDSA. Never acceptnoneor anything else.- The signature verifies with the key from
/jwks.jsonwhosekidmatches the header. issis exactly your issuer URL.audis your client ID.expis in the future (allow a minute of clock skew at most).noncematches the one you sent, if you sent one.
The JWKS
{
"keys": [{
"kty": "OKP", "crv": "Ed25519", "use": "sig", "alg": "EdDSA",
"kid": "2026-10-active-1d8f12",
"x": "en_tDHOxBWpq8_etBFcqV09XqZTC0joJoqPEoq2Trls"
}]
}
Signing keys rotate. A retiring key stays in the JWKS for 35 days so tokens it signed still verify. Cache the JWKS, and refetch it when you see a kid you don’t recognise.
Verify in Node.js with jose
import { createRemoteJWKSet, jwtVerify } from "jose"
const ISSUER = "https://acme.gatekeepauth.com"
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/jwks.json`)) // caches and refetches on new kid
export async function verifyIdToken(idToken, expectedNonce) {
const { payload } = await jwtVerify(idToken, jwks, {
issuer: ISSUER,
audience: "acme-notes",
algorithms: ["EdDSA"],
})
if (expectedNonce && payload.nonce !== expectedNonce) throw new Error("nonce mismatch")
return payload // payload.sub, payload.email
}
Access tokens are not for you
The access token’s aud is the issuer, not your app. It’s meant for Gatekeep’s /userinfo. Don’t use it to authorise requests to your own API by verifying it locally. Instead, sign users in with the ID token and use your own session.