Auth0 JWTs are signed, not encrypted, meaning anyone can decode them and read the payload.

Let’s see a real-world example. Imagine a user logs in through your frontend and receives a JWT. This token is then sent to your backend with every API request. Your backend needs to verify this token to ensure it’s legitimate and hasn’t been tampered with.

Here’s a typical JWT payload:

{
  "iss": "https://your-auth0-domain.auth0.com/",
  "sub": "auth0|1234567890abcdef",
  "aud": "your-api-audience",
  "exp": 1678886400,
  "iat": 1678882800,
  "azp": "your-client-id",
  "scope": "openid profile email read:data"
}

The iss (issuer) tells you who issued the token (your Auth0 domain). The sub (subject) is the unique identifier for the user. aud (audience) specifies who the token is intended for – in this case, your API. exp (expiration) is a Unix timestamp indicating when the token becomes invalid. iat (issued at) is when the token was issued. azp (authorized party) is the client ID that requested the token. scope defines the permissions granted by the token.

To validate these tokens in your backend, you’ll typically use a library. For Node.js, express-jwt and jwks-rsa are common choices.

Here’s a simplified Node.js example using express-jwt and jwks-rsa:

const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const express = require('express');

const app = express();

// Your Auth0 domain and API audience
const AUTH0_DOMAIN = 'your-auth0-domain.auth0.com';
const API_AUDIENCE = 'your-api-audience';

// Middleware to validate JWTs
const checkJwt = jwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`
  }),
  audience: API_AUDIENCE,
  issuer: `https://${AUTH0_DOMAIN}/`,
  algorithms: ['RS256'] // Ensure this matches your Auth0 application's signing algorithm
});

// Apply the middleware to protected routes
app.get('/api/protected-resource', checkJwt, (req, res) => {
  // If the JWT is valid, the decoded payload is available in req.user
  res.json({ message: 'You have access to this protected resource!', user: req.user });
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

In this setup:

  • jwksRsa.expressJwtSecret fetches the public keys from Auth0’s JWKS endpoint (/.well-known/jwks.json). These public keys are used to verify the signature of the JWT.
  • The secret option is configured to use these dynamically fetched keys.
  • audience and issuer are crucial for ensuring the token was intended for your API and issued by your Auth0 tenant.
  • algorithms: ['RS256'] explicitly states the signing algorithm expected. Auth0 typically uses RS256.

The checkJwt middleware intercepts incoming requests. If a valid JWT is present in the Authorization: Bearer <token> header, it verifies the signature, checks the audience and issuer, and ensures the token hasn’t expired. If all checks pass, req.user will contain the decoded JWT payload, and the request proceeds to your route handler. If any check fails, express-jwt will automatically send a 401 Unauthorized response.

The most surprising truth about JWT validation is that you never actually validate the token’s signature against a secret you manage directly. Instead, you retrieve Auth0’s public keys (the JWKS) and use those to verify the signature. This is a form of public-key cryptography where Auth0 signs tokens with its private key, and your backend uses Auth0’s publicly available keys to confirm authenticity without ever needing Auth0’s private key.

The algorithms parameter is critical. If you have multiple signing algorithms configured in Auth0 for different applications or scenarios, you must specify all of them that your backend should accept. If the JWT was signed with an algorithm not listed here, validation will fail. For instance, if Auth0 is configured to also support HS256 (though less common for JWTs issued to third-party APIs) and you only list RS256, the token won’t be accepted.

The next step after successfully validating JWTs is often implementing role-based access control (RBAC) within your backend, using the claims present in the validated JWT payload.

Want structured learning?

Take the full Auth0 course →