> ## Documentation Index
> Fetch the complete documentation index at: https://auth0-docs-event-stream-action-templates.mintlify.site/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 @auth0/auth0-express-api SDK (Beta).

export const AuthCodeGroup = ({children, dropdown}) => {
  const [processedChildren, setProcessedChildren] = useState(children);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      unsubscribe = window.autorun(() => {
        const processChildren = node => {
          if (typeof node === "string") {
            let processedNode = node;
            for (const [key, value] of window.rootStore.variableStore.values.entries()) {
              const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
              processedNode = processedNode.replaceAll(new RegExp(escapedKey, "g"), value);
            }
            return processedNode;
          } else if (Array.isArray(node)) {
            return node.map(processChildren);
          } else if (node && node.props && node.props.children) {
            return {
              ...node,
              props: {
                ...node.props,
                children: processChildren(node.props.children)
              }
            };
          }
          return node;
        };
        setProcessedChildren(processChildren(children));
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  return <CodeGroup dropdown={dropdown}>{processedChildren}</CodeGroup>;
};

export const AuthCodeBlock = ({filename, icon, language, highlight, children}) => {
  const [displayText, setDisplayText] = useState(children);
  const [copyText, setCopyText] = useState(children);
  const wrapperRef = React.useRef(null);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      if (!window.autorun || !window.rootStore) {
        return;
      }
      unsubscribe = window.autorun(() => {
        let processedChildrenForDisplay = children;
        let processedChildrenForCopy = children;
        for (const [key, value] of window.rootStore.variableStore.values.entries()) {
          const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
          let displayValue = value;
          if (key === "{yourClientSecret}" && value !== "{yourClientSecret}") {
            displayValue = value.substring(0, 3) + "*****MASKED*****";
          }
          processedChildrenForDisplay = processedChildrenForDisplay.replaceAll(new RegExp(escapedKey, "g"), displayValue);
          processedChildrenForCopy = processedChildrenForCopy.replaceAll(new RegExp(escapedKey, "g"), value);
        }
        setDisplayText(processedChildrenForDisplay);
        setCopyText(processedChildrenForCopy);
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  useEffect(() => {
    if (!wrapperRef.current) return;
    const originalWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
    let isOverriding = false;
    const handleClick = e => {
      const button = e.target.closest('[data-testid="copy-code-button"]');
      if (!button || !wrapperRef.current.contains(button)) return;
      isOverriding = true;
      navigator.clipboard.writeText = text => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
          return originalWriteText(copyText);
        }
        return originalWriteText(text);
      };
      setTimeout(() => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
        }
      }, 100);
    };
    const wrapper = wrapperRef.current;
    wrapper.addEventListener('click', handleClick, true);
    return () => {
      wrapper.removeEventListener('click', handleClick, true);
      if (navigator.clipboard.writeText !== originalWriteText) {
        navigator.clipboard.writeText = originalWriteText;
      }
    };
  }, [copyText]);
  return <div ref={wrapperRef}>
      <CodeBlock filename={filename} icon={icon} language={language} lines highlight={highlight}>
        {displayText}
      </CodeBlock>
    </div>;
};

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

<HowToSchema />

export const envSnippet = `AUTH0_DOMAIN={yourDomain}
AUTH0_AUDIENCE=YOUR_API_IDENTIFIER`;

export const envSnippetDashboard = `AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
AUTH0_AUDIENCE=YOUR_API_IDENTIFIER`;

<Warning>
  This Quickstart is currently in **Beta**. We'd love to hear your feedback!
</Warning>

<Callout icon="file-lines" color="#0EA5E9" iconType="regular">
  **Prerequisites:** Before you begin, ensure you have the following installed:

  * [Node.js](https://nodejs.org/) 22 LTS or newer
  * [npm](https://www.npmjs.com/) 10+ or [yarn](https://yarnpkg.com/) 1.22+
</Callout>

## 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- and claim-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.

    <AuthCodeGroup>
      ```shellscript Mac theme={null}
      mkdir auth0-express-api && cd auth0-express-api
      npm init -y
      touch server.js .env
      ```

      ```shellscript Windows theme={null}
      mkdir auth0-express-api; cd auth0-express-api
      npm init -y
      New-Item server.js, .env
      ```
    </AuthCodeGroup>

    Update your `package.json` to use ES modules and add start scripts:

    ```json theme={null}
    {
      "name": "auth0-express-api",
      "version": "1.0.0",
      "type": "module",
      "main": "server.js",
      "scripts": {
        "start": "node server.js",
        "dev": "node --watch server.js"
      }
    }
    ```
  </Step>

  <Step title="Install the SDK" stepNumber={2}>
    Install `@auth0/auth0-express-api` along with `express` and `dotenv`:

    ```shell theme={null}
    npm install @auth0/auth0-express-api@beta express dotenv
    ```
  </Step>

  <Step title="Setup your Auth0 API" stepNumber={3}>
    You need to create a new API on your Auth0 tenant and configure your environment variables.

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

        <AuthCodeGroup>
          ```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
          ```
        </AuthCodeGroup>

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

        <AuthCodeBlock children={envSnippet} language="shellscript" filename=".env" />

        Replace `YOUR_API_IDENTIFIER` with the identifier you used above (e.g., `https://my-express-api.example.com`).
      </Tab>

      <Tab title="Dashboard">
        1. Go to [Auth0 Dashboard](https://manage.auth0.com/) → **Applications > APIs** → **Create API**
        2. Enter a name (e.g., "My Express API")
        3. Set the **Identifier** — this is your API audience (e.g., `https://my-express-api.example.com`). It doesn't need to be a real URL.
        4. Keep **Signing Algorithm** as **RS256**
        5. Click **Create**
        6. Copy the **Domain** from your tenant and the **Identifier** from **API Settings**

        Create your `.env` file:

        <AuthCodeBlock children={envSnippetDashboard} language="shellscript" filename=".env" />

        <Callout icon="file-lines" color="#0EA5E9" iconType="regular">
          Replace `YOUR_AUTH0_DOMAIN` with your Auth0 tenant domain (e.g., `dev-abc123.us.auth0.com`) and `YOUR_API_IDENTIFIER` with the API identifier you set above.
        </Callout>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Configure the JWT middleware" stepNumber={4}>
    Register `createAuth0Api()` on your Express application to configure JWT validation. Then add public and protected routes.

    ```javascript server.js theme={null}
    import 'dotenv/config';
    import express from 'express';
    import { createAuth0Api, requiresAuth } from '@auth0/auth0-express-api';

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

    app.use(express.json());
    app.use(createAuth0Api());

    // Public route — no token required
    app.get('/api/public', (req, res) => {
      res.json({
        message: 'Hello from a public endpoint! No authentication required.',
        timestamp: new Date().toISOString(),
      });
    });

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

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

    **What this does:**

    * `createAuth0Api()` reads `AUTH0_DOMAIN` and `AUTH0_AUDIENCE` from environment variables automatically
    * `requiresAuth()` validates the `Authorization: Bearer <token>` header on each request
    * `req.auth0.user` contains the decoded JWT claims for authenticated requests — `sub` is the user's unique identifier
  </Step>

  <Step title="Protect a route with a required scope" stepNumber={5}>
    Beyond requiring a valid token, you can require a specific scope. Pass a `scopes` option to `requiresAuth()` — the SDK returns `403 insufficient_scope` if the token is missing the scope.

    ```javascript server.js theme={null}
    // Requires the "read:messages" scope
    app.get('/api/messages', requiresAuth({ scopes: ['read:messages'] }), (req, res) => {
      res.json({ messages: ['Hello!', 'World!'] });
    });
    ```

    <Callout icon="file-lines" color="#0EA5E9" iconType="regular">
      Define the scope in your API's **Permissions** tab (see [Advanced Usage](#advanced-usage)) and request it when obtaining the access token. For matching multiple scopes or authorizing on custom claims, the SDK also provides `scopesInclude`, `claimEquals`, `claimIncludes`, and `claimCheck` — covered in [Advanced Usage](#advanced-usage).
    </Callout>
  </Step>

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

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

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

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

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

    Expected response:

    ```json theme={null}
    {
      "message": "Hello from a public endpoint! No authentication required.",
      "timestamp": "2026-06-22T12:00:00.000Z"
    }
    ```

    To call the protected endpoint, you need an access 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 the protected endpoint:

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

    Expected response:

    ```json theme={null}
    {
      "message": "Hello from a protected endpoint! You are authenticated.",
      "sub": "auth0|abc123...",
      "timestamp": "2026-06-22T12:00:00.000Z"
    }
    ```

    <Check>
      **Checkpoint**

      You should now have a protected API. Your API:

      1. Accepts requests to public endpoints without a token
      2. Returns the protected response when a valid access token is provided
      3. Validates JWTs against your Auth0 domain and audience
      4. Exposes decoded token claims via `req.auth0.user`
    </Check>
  </Step>
</Steps>

***

## Advanced Usage

<AccordionGroup>
  <Accordion title="Matching multiple scopes with scopesInclude">
    Use `scopesInclude` when a route should accept any one of several scopes, or require several at once. By default it matches **any** of the listed scopes; pass `{ match: 'all' }` to require all of them. Scopes can be passed as an array or a space-separated string — these examples use arrays.

    ```javascript server.js theme={null}
    import { scopesInclude } from '@auth0/auth0-express-api';

    // Requires ANY of these scopes
    app.get('/api/feed', requiresAuth(), scopesInclude(['read:feed', 'read:admin']), (req, res) => {
      res.json({ feed: [] });
    });

    // Requires ALL of these scopes
    app.get('/api/admin/edit', requiresAuth(), scopesInclude(['read:admin', 'write:admin'], { match: 'all' }), (req, res) => {
      res.json({ message: 'Admin editor access granted.' });
    });
    ```
  </Accordion>

  <Accordion title="Authorizing on custom claims">
    When authorization depends on claims other than `scope`, use `claimEquals`, `claimIncludes`, or `claimCheck`. Each runs after `requiresAuth()` and returns `401 invalid_token` if the claim requirement isn't met.

    ```javascript server.js theme={null}
    import { claimEquals, claimIncludes, claimCheck } from '@auth0/auth0-express-api';

    // claimEquals — claim must equal an exact value
    app.get('/api/admin', requiresAuth(), claimEquals('isAdmin', true), (req, res) => {
      res.json({ message: 'Admin access granted.' });
    });

    // claimIncludes — array claim must contain all listed values
    app.get('/api/editor', requiresAuth(), claimIncludes('roles', ['admin', 'editor']), (req, res) => {
      res.json({ message: 'Editor access granted.' });
    });

    // claimCheck — custom logic over the decoded token
    app.get('/api/premium', requiresAuth(), claimCheck(
      (req, token) => token.tier === 'premium' || token.roles?.includes('admin'),
      { errorMessage: 'Premium tier or admin role required' }
    ), (req, res) => {
      res.json({ message: 'Premium content access granted.' });
    });
    ```
  </Accordion>

  <Accordion title="Defining custom token claims with TypeScript">
    If you're using TypeScript, augment the `Token` interface to get type-safe access to custom claims:

    ```typescript server.ts theme={null}
    import '@auth0/auth0-express-api';

    declare module '@auth0/auth0-express-api' {
      interface Token {
        tier: 'free' | 'premium';
        roles: string[];
        'https://myapp.com/org_id': string;
      }
    }
    ```

    Install type support:

    ```shell theme={null}
    npm install -D typescript @types/express @types/node
    ```
  </Accordion>

  <Accordion title="CORS configuration for web clients">
    Enable CORS to allow your web application to call the API:

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

    ```javascript server.js theme={null}
    import cors from 'cors';

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

    app.use(createAuth0Api());
    ```

    For production, specify exact allowed origins instead of wildcards.
  </Accordion>

  <Accordion title="Configuring scopes in the Auth0 Dashboard">
    To use scope-based authorization, first define the permissions on your API:

    1. Go to [Auth0 Dashboard](https://manage.auth0.com/) → **Applications > APIs** → your API
    2. Navigate to the **Permissions** tab
    3. Add permissions like `read:messages`, `write:messages`, `read:admin`
    4. Click **Save**

    Your client application must then request these scopes when obtaining an access token. If a token lacks the required scope, the API returns `403 Forbidden`.
  </Accordion>
</AccordionGroup>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 with an empty body and a bare 'WWW-Authenticate: Bearer' header">
    **Cause:** The `Authorization` header is missing or malformed, so no bearer token could be extracted. Per [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750#section-3), the SDK returns `401` with a bare `WWW-Authenticate: Bearer` header and no error body in this case. This is distinct from a token that is *present but invalid or expired*, which returns `401` with an `invalid_token` error and a JSON body (see below).

    **Fix:**

    1. Ensure the header is present: `Authorization: Bearer YOUR_TOKEN`
    2. Verify "Bearer" (with a capital B and a space) precedes the token
  </Accordion>

  <Accordion title="'Invalid token' or audience/issuer mismatch (401)">
    **Cause:** The token was not issued for this API, or the domain/audience values don't match.

    **Fix:**

    1. Decode your token at [jwt.io](https://jwt.io)
    2. Check `iss` matches `https://{yourDomain}/` (note the trailing slash)
    3. Check `aud` matches your `AUTH0_AUDIENCE` exactly
    4. Make sure you're using an **access token**, not an ID token — access tokens are obtained with the `audience` parameter
  </Accordion>

  <Accordion title="'Insufficient scope' (403)">
    **Cause:** The token does not include the required scope.

    **Fix:**

    1. Verify the scope is defined in your API's Permissions tab in the Auth0 Dashboard
    2. Ensure the client is requesting the scope when obtaining the access token
    3. Decode the token at [jwt.io](https://jwt.io) and check the `scope` claim
  </Accordion>

  <Accordion title="Environment variables not loaded">
    **Cause:** `dotenv` is not configured, or variable names are wrong.

    **Fix:**

    1. Ensure `import 'dotenv/config'` is the first import in your entry file
    2. Verify `.env` contains `AUTH0_DOMAIN` and `AUTH0_AUDIENCE`
    3. Debug:

    ```javascript theme={null}
    console.log({
      domain: !!process.env.AUTH0_DOMAIN,
      audience: !!process.env.AUTH0_AUDIENCE,
    });
    ```
  </Accordion>

  <Accordion title="ESM import errors ('Cannot use import statement')">
    **Cause:** The `@auth0/auth0-express-api` SDK uses ES modules.

    **Fix:** Add `"type": "module"` to your `package.json`:

    📁 **package.json**

    ```json theme={null}
    {
      "type": "module"
    }
    ```

    Or rename your server file to `server.mjs`.
  </Accordion>
</AccordionGroup>

***

## Next Steps

* **[Add Login to an Express Web App](/docs/quickstart/webapp/express-beta)** — Use `@auth0/auth0-express` for session-based auth in web apps
* **[Role-Based Access Control](https://auth0.com/docs/manage-users/access-control/rbac)** — Implement fine-grained permissions
* **[Access Token Best Practices](https://auth0.com/docs/secure/tokens/access-tokens)** — Learn about access token handling
* **[Monitor Your API](https://auth0.com/docs/deploy-monitor/logs)** — Set up logging and monitoring

***

## Resources

* **[auth0/auth0-express-api GitHub](https://github.com/auth0/auth0-express/tree/main/packages/auth0-express-api)** — Source code and examples
* **[Auth0 Community](https://community.auth0.com/)** — Get help from the community
* **[JWT.io](https://jwt.io/)** — Debug and decode JWTs
