Express OAuth2 JWT Bearer
raw JSON → 1.8.0 verified Sat Apr 25 auth: no javascript
Authentication middleware for Express.js that validates JWT Bearer access tokens issued by an OAuth 2.0 authorization server. Version 1.8.0 supports DPoP (Proof-of-Possession) authentication, multiple custom domains, clock tolerance for nbf claim, and Node.js versions 12 through 24. Maintained by Auth0 with 100% test coverage. Differentiates from other JWT middleware by being Auth0-optimized, supporting OAuth 2.0 token validation (not just JWT decoding), and providing built-in security headers guidance.
Common errors
error express-oauth2-jwt-bearer: UnauthorizedError: jwt issuer is not configured ↓
cause Missing issuerBaseURL or issuer option in auth() config.
fix
Add issuerBaseURL to auth(): auth({ issuerBaseURL: 'https://your-tenant.auth0.com/', audience: '...' })
error TypeError: Cannot destructure property 'auth' of (intermediate value) as it is undefined ↓
cause Incorrect import: using default import instead of named import.
fix
Use named import: import { auth } from 'express-oauth2-jwt-bearer' (or const { auth } = require(...) for CJS)
error jwks-rsa: Error: unable to get local issuer certificate ↓
cause HTTPS TLS certificate validation failed, often due to self-signed certs or corporate proxies.
fix
Set NODE_TLS_REJECT_UNAUTHORIZED=0 (only for development) or configure proper CA certificates.
Warnings
breaking In v1.7.3, the unauthorized response status code changed from 401 to 403 for insufficient scope errors, as per RFC 6750. ↓
fix Update to v1.7.3 or later. If you relied on the previous 401 status, adjust your error handling logic.
gotcha The library silently changes 401 to 403 in some error conditions; always use err.status from the error object, not a hardcoded value. ↓
fix In error handling middleware, use err.status (e.g., if (err.status === 403) {...}) instead of assuming 401.
deprecated The 'secret' option (for symmetric algorithms like HS256) is deprecated since v1.4.0 and discouraged for production; use asymmetric keys (RS256) via issuerBaseURL. ↓
fix Use issuerBaseURL with a JWKS endpoint instead of secret. If you must use HS256, ensure secret is a strong symmetric key.
gotcha When using issuer (not issuerBaseURL) for symmetric algorithms, you must also specify tokenSigningAlg: 'HS256'. Omitting it defaults to RS256 which will fail. ↓
fix Specify tokenSigningAlg: 'HS256' when using symmetric secret. Example: auth({ issuer: '...', audience: '...', secret: '...', tokenSigningAlg: 'HS256' })
gotcha DPoP authentication is in early access; the default mode accepts both DPoP and Bearer tokens. To enforce DPoP-only, you must set dpop: { enforce: true }. ↓
fix Read EXAMPLES.md for full DPoP config options. Set dpop: { enforce: true } to require DPoP proof.
Install
npm install express-oauth2-jwt-bearer yarn add express-oauth2-jwt-bearer pnpm add express-oauth2-jwt-bearer Imports
- auth wrong
const auth = require('express-oauth2-jwt-bearer')correctimport { auth } from 'express-oauth2-jwt-bearer' - VerificationError wrong
import { UnauthorizedError } from 'express-oauth2-jwt-bearer'correctimport { VerificationError } from 'express-oauth2-jwt-bearer' - ClaimCheckError wrong
import { CheckError } from 'express-oauth2-jwt-bearer'correctimport { ClaimCheckError } from 'express-oauth2-jwt-bearer'
Quickstart
import { auth, requiredScopes } from 'express-oauth2-jwt-bearer';
import express from 'express';
const app = express();
// Validate JWT bearer token
app.use(
auth({
issuerBaseURL: process.env.ISSUER_BASE_URL ?? 'https://example.auth0.com',
audience: process.env.AUDIENCE ?? 'https://api.example.com',
})
);
// Protected route
app.get('/api/messages', requiredScopes('read:messages'), (req, res) => {
res.json({
message: 'Access granted',
payload: req.auth.payload,
});
});
// Error handling (Express default handles err.status and err.headers)
const port = process.env.PORT ?? 3000;
app.listen(port, () => console.log(`Server running on port ${port}`));