Verifying JWTs in Node.js
Validate EmbedAuth's RS256 tokens on a Node.js backend using jose — with a reusable verifier, Express middleware, and the checks you must never skip.
A token from the browser is just a claim until your server checks its signature. This guide verifies EmbedAuth's RS256 JWTs in Node.js using jose, the most widely used JOSE implementation for the platform.
Install
npm install jose
The verifier
EmbedAuth publishes its public keys at a JWKS endpoint. createRemoteJWKSet fetches and caches those keys, and automatically selects the right one by the token's kid header — so key rotation requires no code change on your side.
import { createRemoteJWKSet, jwtVerify } from 'jose';
const CLIENT_ID = process.env.EMBEDAUTH_CLIENT_ID;
const JWKS = createRemoteJWKSet(
new URL(`https://embedauth.com/.well-known/jwks.json?clientId=${CLIENT_ID}`)
);
export async function verifyToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://embedauth.com',
audience: CLIENT_ID,
});
return payload; // { sub, email, name, clientId, iat, exp }
}
Create the JWKS set once at module scope, not per request. It maintains an in-memory cache and a cooldown so it will not hammer the endpoint.
The three checks that matter
jwtVerify does more than confirm the signature. Passing issuer and audience is what turns a valid-looking token into a token that is valid for you:
- Signature — proves EmbedAuth issued the token and it was not tampered with.
issuer— must equalhttps://embedauth.com. Rejects tokens minted by anything else.audience— must equal yourclientId. Rejects a genuine EmbedAuth token that was issued for a different app. Skipping this is a common and serious mistake.
Expiry (exp) is checked automatically. If you omit audience, a token issued to another EmbedAuth customer would pass — so always pin it.
Express middleware
import express from 'express';
import { verifyToken } from './verifyToken.js';
const app = express();
async function requireAuth(req, res, next) {
const header = req.headers.authorization ?? '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: 'missing_token' });
try {
req.user = await verifyToken(token);
next();
} catch (err) {
res.status(401).json({ error: 'invalid_token', reason: err.code });
}
}
app.get('/api/me', requireAuth, (req, res) => {
res.json({ id: req.user.sub, email: req.user.email });
});
Establishing your own session
Verifying the EmbedAuth token proves who the user is. For anything beyond a single request, mint your own first-party session rather than re-sending the EmbedAuth token on every call. On first verification, upsert the user in your database keyed on payload.sub, then issue a session cookie or token whose lifetime and revocation you control. This decouples your app's session policy from EmbedAuth's token expiry.
Error handling
jose throws typed errors you can branch on:
Error code | Cause | Response |
|---|---|---|
ERR_JWT_EXPIRED | Token past exp | 401 — prompt re-authentication |
ERR_JWS_SIGNATURE_VERIFICATION_FAILED | Wrong key / tampering | 401 — reject |
ERR_JWT_CLAIM_VALIDATION_FAILED | issuer/audience mismatch | 401 — reject |
ERR_JWKS_NO_MATCHING_KEY | Unknown kid | 401 — reject, do not retry in a loop |
Never log the raw token, and never fall back to accepting an unverified token when verification fails.
Next steps
- Frontend that produces the token: React or Next.js.
- Other backends: Python, Go.
- Background on the token format: JWT Explained for Developers.