Backend & Token Verification

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.

2 min readUpdated July 5, 2026

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:

  1. Signature — proves EmbedAuth issued the token and it was not tampered with.
  2. issuer — must equal https://embedauth.com. Rejects tokens minted by anything else.
  3. audience — must equal your clientId. 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 codeCauseResponse
ERR_JWT_EXPIREDToken past exp401 — prompt re-authentication
ERR_JWS_SIGNATURE_VERIFICATION_FAILEDWrong key / tampering401 — reject
ERR_JWT_CLAIM_VALIDATION_FAILEDissuer/audience mismatch401 — reject
ERR_JWKS_NO_MATCHING_KEYUnknown kid401 — 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