> ## Documentation Index
> Fetch the complete documentation index at: https://auth0.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Protect Your Express.js API

> This guide demonstrates how to protect Express.js API endpoints using JWT access tokens with the express-oauth2-jwt-bearer SDK.

export const HowToSchema = () => <script type="application/ld+json">
    {'{"@context":"https://schema.org","@type":"HowTo"}'}
  </script>;

<HowToSchema />

<Callout icon="pencil" color="#FFC107" iconType="solid">
  A new **Beta** version of this quickstart is available using the `@auth0/auth0-express-api` SDK, which will soon replace this guide. [Try the Beta quickstart →](/docs/quickstart/backend/express-api-beta)
</Callout>

<Accordion title="Use AI to integrate Auth0" icon="microchip-ai" iconType="solid" defaultOpen>
  If you use an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you can add Auth0 API authentication automatically in minutes using [agent skills](https://agentskills.io/home).

  **Install:**

  ```bash theme={null}
  npx skills add auth0/agent-skills --skill auth0-quickstart --skill auth0-express-api
  ```

  **Then ask your AI assistant:**

  ```text theme={null}
  Add Auth0 JWT authentication to my Express API
  ```

  Your AI assistant will automatically create your Auth0 API, fetch credentials, install `express-oauth2-jwt-bearer`, configure the JWT middleware, and protect your API endpoints with token validation. [Full agent skills documentation →](/docs/quickstart/agent-skills)
</Accordion>

<Note>
  **Prerequisites:** Before you begin, ensure you have the following installed:

  * **[Node.js](https://nodejs.org/en/download)** 18 LTS or newer (supports `^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0`)
  * **[npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)** 8+ or **[yarn](https://classic.yarnpkg.com/lang/en/docs/install/)** 1.22+ or **[pnpm](https://pnpm.io/installation)** 8+

  Verify installation: `node --version && npm --version`

  **Express Version Compatibility:** This quickstart works with **Express 4.x** and **Express 5.x**.
</Note>

## Get Started

This quickstart demonstrates how to protect Express.js API endpoints using JWT access tokens. You'll build a secure API that validates Auth0 access tokens, protects routes, and implements scope-based authorization.

<Steps>
  <Step title="Create a new project" stepNumber={1}>
    Create a new directory for your Express API and initialize a Node.js project.

    ```shellscript theme={null}
    mkdir auth0-express-api && cd auth0-express-api
    ```

    Initialize the project

    ```shellscript theme={null}
    npm init -y
    ```

    Create the project structure

    ```shellscript theme={null}
    touch server.js .env
    ```
  </Step>

  <Step title="Install the express-oauth2-jwt-bearer SDK" stepNumber={2}>
    Install the required dependencies

    ```shellscript theme={null}
    npm install express express-oauth2-jwt-bearer dotenv
    ```

    Add start scripts to your `package.json`:

    ```json package.json theme={null}
    {
      "scripts": {
        "start": "node server.js",
        "dev": "node --watch server.js"
      }
    }
    ```
  </Step>

  <Step title="Setup your Auth0 API" stepNumber={3}>
    Next, you need to create a new API on your Auth0 tenant and add the environment variables to your project.

    You have two options to set up your Auth0 API: use a CLI command or configure manually via the Dashboard:

    <Tabs>
      <Tab title="CLI">
        Run the following command in your project's root directory to create an Auth0 API:

        <CodeGroup>
          ```shellscript Mac theme={null}
          # Install Auth0 CLI (if not already installed)
          brew tap auth0/auth0-cli && brew install auth0

          # Create Auth0 API
          auth0 apis create \
            --name "My Express API" \
            --identifier https://my-express-api.example.com
          ```

          ```powershell Windows theme={null}
          # Install Auth0 CLI (if not already installed)
          scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git
          scoop install auth0

          # Create Auth0 API
          auth0 apis create `
            --name "My Express API" `
            --identifier https://my-express-api.example.com
          ```
        </CodeGroup>

        <Note>
          This command will:

          1. Check if you're authenticated (and prompt for login if needed)
          2. Create an Auth0 API with the specified identifier
          3. Display the API details including the domain and identifier
        </Note>

        After creation, copy the **Identifier** and your **Domain** values, then create your `.env` file:

        ```bash .env theme={null}
        AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
        AUTH0_AUDIENCE=YOUR_API_IDENTIFIER
        ```

        <Note>
          Replace `YOUR_AUTH0_DOMAIN` with your Auth0 tenant domain (e.g., `dev-abc123.us.auth0.com`) and `YOUR_API_IDENTIFIER` with your API identifier (e.g., `https://my-express-api.example.com`).
        </Note>
      </Tab>

      <Tab title="Dashboard">
        1. Go to the [Auth0 Dashboard](https://manage.auth0.com/dashboard/)
        2. Navigate to **Applications** → **APIs** → **Create API**
        3. Enter a name for your API (e.g., "My Express API")
        4. Set the **Identifier** (e.g., `https://my-express-api.example.com`)
           * This is your API audience and must be a valid URL format
           * It doesn't need to be a real URL - it's just an identifier
        5. Keep **Signing Algorithm** as **RS256**
        6. Click **Create**
        7. Copy the **Identifier** value from the **Settings** tab

        Create your `.env` file with the following values:

        ```bash .env theme={null}
        AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
        AUTH0_AUDIENCE=YOUR_API_IDENTIFIER
        ```

        <Note>
          Replace `YOUR_AUTH0_DOMAIN` with your Auth0 tenant domain (e.g., `dev-abc123.us.auth0.com`) and `YOUR_API_IDENTIFIER` with your API identifier from the dashboard (e.g., `https://my-express-api.example.com`).
        </Note>
      </Tab>
    </Tabs>

    <Tip>
      Verify your `.env` file exists: `cat .env` (Mac/Linux) or `type .env` (Windows)
    </Tip>
  </Step>

  <Step title="Configure the JWT middleware" stepNumber={4}>
    Create your Express server and configure JWT validation:

    ```javascript server.js {1-3,6-7,10-13} lines theme={null}
    require('dotenv').config();
    const express = require('express');
    const { auth } = require('express-oauth2-jwt-bearer');

    const app = express();
    const port = process.env.PORT || 3001;

    // Configure JWT validation middleware
    const checkJwt = auth({
      issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
      audience: process.env.AUTH0_AUDIENCE,
    });

    // Start server
    app.listen(port, () => {
      console.log(`API server running at http://localhost:${port}`);
    });
    ```

    **What this does:**

    * Creates JWT validation middleware using your Auth0 domain and API audience
    * Validates the `iss` and `aud` claims on incoming access tokens
    * Makes `checkJwt` available for protecting individual routes
  </Step>

  <Step title="Create API routes" stepNumber={5}>
    Add public and protected routes to your `server.js`:

    ```javascript server.js expandable lines theme={null}
    require('dotenv').config();
    const express = require('express');
    const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

    const app = express();
    const port = process.env.PORT || 3001;

    // Configure JWT validation middleware
    const checkJwt = auth({
      issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
      audience: process.env.AUTH0_AUDIENCE,
    });

    // Public route - no authentication required
    app.get('/api/public', (req, res) => {
      res.json({
        message: 'Hello from a public endpoint! You don\'t need to be authenticated to see this.',
        timestamp: new Date().toISOString(),
      });
    });

    // Protected route - requires valid access token
    app.get('/api/private', checkJwt, (req, res) => {
      res.json({
        message: 'Hello from a protected endpoint! You successfully authenticated.',
        user: req.auth.payload.sub,
        timestamp: new Date().toISOString(),
      });
    });

    // Protected route with scope - requires 'read:messages' scope
    app.get('/api/private-scoped', checkJwt, requiredScopes('read:messages'), (req, res) => {
      res.json({
        message: 'Hello from a scoped endpoint! You have the required permission.',
        user: req.auth.payload.sub,
        scope: req.auth.payload.scope,
        timestamp: new Date().toISOString(),
      });
    });

    // Error handling middleware
    app.use((err, req, res, next) => {
      const status = err.status || 500;
      const message = err.message || 'Internal Server Error';

      res.status(status).json({
        error: err.code || 'server_error',
        message: status === 401 ? 'Authentication required' : message,
      });
    });

    // Start server
    app.listen(port, () => {
      console.log(`API server running at http://localhost:${port}`);
    });
    ```

    **Key points:**

    * Public routes don't require authentication
    * Protected routes use the `checkJwt` middleware to require a valid JWT
    * Scoped routes use `requiredScopes()` to require specific permissions in the token
    * `req.auth.payload` contains the decoded JWT claims for authenticated requests
    * The `sub` claim contains the user's unique identifier
  </Step>

  <Step title="Run your API" stepNumber={6}>
    Start the development server:

    ```shellscript theme={null}
    npm run dev
    ```

    Your API is now running at [http://localhost:3001](http://localhost:3001).

    <Info>
      The `--watch` flag in Node.js 18+ automatically restarts the server when files change.
    </Info>
  </Step>

  <Step title="Test your API" stepNumber={7}>
    Test the public endpoint (no authentication required):

    ```bash theme={null}
    curl http://localhost:3001/api/public
    ```

    You should see:

    ```json theme={null}
    {
      "message": "Hello from a public endpoint! You don't need to be authenticated to see this.",
      "timestamp": "2024-01-15T10:30:00.000Z"
    }
    ```

    Test the protected endpoint without a token (should fail):

    ```bash theme={null}
    curl http://localhost:3001/api/private
    ```

    You should see a 401 Unauthorized error:

    ```json theme={null}
    {
      "error": "unauthorized",
      "message": "Authentication required"
    }
    ```

    To test with a valid token:

    1. Go to [Auth0 Dashboard](https://manage.auth0.com/) → **Applications** → **APIs**
    2. Select your API → **Test** tab
    3. Copy the generated access token

    Test your protected endpoint:

    ```bash theme={null}
    curl http://localhost:3001/api/private \
      -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
    ```

    You should see:

    ```json theme={null}
    {
      "message": "Hello from a protected endpoint! You successfully authenticated.",
      "user": "auth0|abc123...",
      "timestamp": "2024-01-15T10:30:00.000Z"
    }
    ```
  </Step>
</Steps>

<Check>
  **Checkpoint**

  You should now have a protected API. Your API:

  1. Accepts requests to public endpoints without authentication
  2. Rejects requests to protected endpoints without a valid token
  3. Validates JWT tokens against your Auth0 domain and audience
  4. Provides user information from the token claims via `req.auth.payload`
</Check>

***

## Advanced Usage

<Accordion title="Scope-Based Authorization">
  Scopes allow fine-grained access control. You can require specific scopes for different endpoints.

  **Configure scopes in Auth0:**

  1. In the [Auth0 Dashboard](https://manage.auth0.com/), go to **Applications** → **APIs** → Your API
  2. Navigate to the **Permissions** tab
  3. Add permissions like `read:messages`, `write:messages`, `admin:access`

  **Protect routes with scopes:**

  ```javascript server.js theme={null}
  const { auth, requiredScopes } = require('express-oauth2-jwt-bearer');

  // Requires 'read:messages' scope
  app.get('/api/messages', checkJwt, requiredScopes('read:messages'), (req, res) => {
    res.json({
      messages: [
        { id: 1, text: 'Hello!' },
        { id: 2, text: 'World!' },
      ],
    });
  });

  // Requires 'admin:access' scope
  app.get('/api/admin', checkJwt, requiredScopes('admin:access'), (req, res) => {
    res.json({
      message: 'Admin access granted',
      userId: req.auth.payload.sub,
    });
  });
  ```

  <Note>
    If a request lacks the required scope, the API returns `403 Forbidden` with an `insufficient_scope` error. Ensure the client application requests the correct scopes when obtaining an access token.
  </Note>
</Accordion>

<Accordion title="Custom Claim Validation">
  Beyond scopes, you can validate custom claims in the JWT payload:

  ```javascript server.js theme={null}
  const { auth, claimEquals, claimIncludes, claimCheck } = require('express-oauth2-jwt-bearer');

  // Require exact claim value
  app.get('/api/org/:orgId',
    checkJwt,
    claimEquals('org_id', 'org_123'),
    (req, res) => {
      res.json({ message: 'Organization access granted' });
    }
  );

  // Require claim to include all specified values
  app.get('/api/roles',
    checkJwt,
    claimIncludes('roles', 'editor', 'viewer'),
    (req, res) => {
      res.json({ message: 'Role check passed' });
    }
  );

  // Custom claim validation logic
  app.get('/api/premium',
    checkJwt,
    claimCheck((claims) => {
      return claims.subscription === 'premium' && claims.verified === true;
    }),
    (req, res) => {
      res.json({ message: 'Premium feature access granted' });
    }
  );
  ```

  <Note>
    Custom claims must use namespaced URLs (e.g., `https://myapp.com/roles`) unless they're standard OIDC claims. [Learn more about custom claims](https://auth0.com/docs/secure/tokens/json-web-tokens/create-custom-claims).
  </Note>
</Accordion>

<Accordion title="Optional Authentication (Mixed Public/Private Routes)">
  Allow both authenticated and anonymous access to the same route:

  ```javascript server.js theme={null}
  const optionalAuth = auth({
    issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
    audience: process.env.AUTH0_AUDIENCE,
    authRequired: false,
  });

  app.get('/api/feed', optionalAuth, (req, res) => {
    if (req.auth) {
      res.json({
        message: `Welcome back, ${req.auth.payload.sub}!`,
        personalizedContent: true,
      });
    } else {
      res.json({
        message: 'Welcome, guest!',
        personalizedContent: false,
      });
    }
  });
  ```
</Accordion>

<Accordion title="CORS Configuration">
  Enable CORS to allow requests from web applications:

  ```bash theme={null}
  npm install cors
  ```

  ```javascript server.js theme={null}
  const cors = require('cors');

  app.use(cors({
    origin: ['http://localhost:3000', 'http://localhost:5173'],
    allowedHeaders: ['Authorization', 'Content-Type'],
    exposedHeaders: ['WWW-Authenticate'],
  }));
  ```

  For production, specify exact origins:

  ```javascript server.js theme={null}
  app.use(cors({
    origin: [
      'https://myapp.com',
      'https://www.myapp.com'
    ],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
  }));
  ```
</Accordion>

<Accordion title="Custom Error Handling">
  Add comprehensive error handling for authentication errors:

  ```javascript server.js theme={null}
  const { UnauthorizedError, InvalidTokenError, InsufficientScopeError } = require('express-oauth2-jwt-bearer');

  app.use((err, req, res, next) => {
    if (err instanceof InsufficientScopeError) {
      return res.status(403).json({
        error: 'forbidden',
        message: 'You do not have permission to access this resource',
        required_scopes: err.requiredScopes,
      });
    }

    if (err instanceof InvalidTokenError) {
      return res.status(401).json({
        error: 'invalid_token',
        message: 'The provided token is invalid or expired',
      });
    }

    if (err instanceof UnauthorizedError) {
      return res.status(401).set(err.headers).json({
        error: 'unauthorized',
        message: 'Authentication required',
      });
    }

    next(err);
  });
  ```
</Accordion>

<Accordion title="TypeScript Support">
  For TypeScript projects, install type definitions and configure your project:

  ```bash theme={null}
  npm install -D typescript @types/express @types/node
  ```

  Create `server.ts`:

  ```typescript server.ts theme={null}
  import 'dotenv/config';
  import express, { Request, Response, NextFunction } from 'express';
  import { auth, requiredScopes, UnauthorizedError } from 'express-oauth2-jwt-bearer';

  const app = express();
  const port = process.env.PORT || 3001;

  const checkJwt = auth({
    issuerBaseURL: `https://${process.env.AUTH0_DOMAIN}`,
    audience: process.env.AUTH0_AUDIENCE,
  });

  app.get('/api/public', (req: Request, res: Response) => {
    res.json({ message: 'Public endpoint - no authentication required' });
  });

  app.get('/api/private', checkJwt, (req: Request, res: Response) => {
    res.json({
      message: 'Private endpoint',
      user: req.auth?.payload.sub,
    });
  });

  app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    if (err instanceof UnauthorizedError) {
      res.status(err.status).set(err.headers).json({
        error: err.code || 'unauthorized',
        message: 'Authentication required',
      });
    } else {
      res.status(500).json({
        error: 'server_error',
        message: 'Internal Server Error',
      });
    }
  });

  app.listen(port, () => {
    console.log(`API server running at http://localhost:${port}`);
  });
  ```

  Add a `tsconfig.json`:

  ```json tsconfig.json theme={null}
  {
    "compilerOptions": {
      "target": "ES2020",
      "module": "commonjs",
      "strict": true,
      "esModuleInterop": true,
      "skipLibCheck": true,
      "outDir": "./dist"
    },
    "include": ["*.ts"]
  }
  ```

  Run with: `npx ts-node server.ts`
</Accordion>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Common Issues and Solutions">
    ### "No authorization token was found"

    **Problem:** The API cannot find the access token in the request.

    **Solutions:**

    1. Ensure the `Authorization` header is present: `Authorization: Bearer YOUR_TOKEN`
    2. Check that "Bearer" is included before the token
    3. Verify the token is not expired

    ### "Invalid token" or "jwt malformed"

    **Problem:** The token format is invalid.

    **Solutions:**

    1. Ensure you're using an **access token**, not an ID token
    2. The token should be obtained with your API's `audience` parameter
    3. Check that the token is a valid JWT (should have three parts separated by dots)

    ### Unexpected "iss" or "aud" value

    **Problem:** The issuer or audience in the token doesn't match your configuration.

    **Solutions:**

    1. Decode your token at [jwt.io](https://jwt.io)
    2. Check the `iss` claim matches `https://YOUR_AUTH0_DOMAIN/` (note the trailing slash)
    3. Check the `aud` claim matches your `AUTH0_AUDIENCE` exactly
    4. Verify your `.env` values:

    ```bash theme={null}
    AUTH0_DOMAIN=dev-abc123.us.auth0.com
    AUTH0_AUDIENCE=https://my-express-api.example.com
    ```

    ### "You must provide an issuerBaseURL" or "audience is required"

    **Problem:** Environment variables are not being loaded.

    **Solutions:**

    1. Ensure `.env` file exists in your project root
    2. Verify `dotenv` is installed: `npm install dotenv`
    3. Add `require('dotenv').config()` at the very top of your server file
    4. Check variable names match exactly (case-sensitive)

    ### 401 Unauthorized on all requests

    **Possible causes:**

    * Token is expired
    * Audience doesn't match
    * Issuer doesn't match

    **Debug steps:**

    1. Decode your token at [jwt.io](https://jwt.io)
    2. Check the `exp` claim hasn't passed
    3. Verify `aud` claim matches your `AUTH0_AUDIENCE` exactly
    4. Verify `iss` claim is `https://{AUTH0_DOMAIN}/`
    5. Ensure Authorization header format is `Bearer YOUR_TOKEN` (with space)

    ### 403 Forbidden with "insufficient\_scope"

    **Problem:** The token doesn't have the required scopes.

    **Solutions:**

    1. Verify the scopes are defined in your Auth0 API (Dashboard → **Applications** → **APIs** → **Permissions**)
    2. Request the scopes when obtaining the token
    3. Check the token's `scope` claim includes the required scopes

    ### CORS errors in browser

    **Problem:** Browser blocks API requests due to CORS policy.

    **Solution:** Install and configure `cors`:

    ```bash theme={null}
    npm install cors
    ```

    ```javascript theme={null}
    const cors = require('cors');

    app.use(cors({
      origin: 'http://localhost:3000',
    }));
    ```
  </Accordion>
</AccordionGroup>

***

## Next Steps

Now that you have a protected API, consider exploring:

* **[Role-Based Access Control](https://auth0.com/docs/manage-users/access-control/rbac)** - Implement fine-grained permissions
* **[API Authorization Best Practices](https://auth0.com/docs/secure/tokens/access-tokens)** - Learn about access token best practices
* **[Monitor Your API](https://auth0.com/docs/deploy-monitor/logs)** - Set up logging and monitoring
* **[Auth0 Community](https://community.auth0.com/)** - Get help from the community

***

## Resources

* **[express-oauth2-jwt-bearer GitHub](https://github.com/auth0/node-oauth2-jwt-bearer/tree/main/packages/express-oauth2-jwt-bearer)** - Source code and examples
* **[Express.js Documentation](https://expressjs.com/)** - Learn more about Express
* **[Auth0 API Authentication](https://auth0.com/docs/secure/tokens/access-tokens)** - Understanding access tokens
* **[JWT.io](https://jwt.io/)** - Debug and decode JWTs
