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

# Authentication

> Protect MCP endpoints with JWT-based authentication using the mcpAuthentication policy.

Agentgateway implements the [MCP Authorization specification](https://modelcontextprotocol.io/specification/2025-11-05/basic/authorization), allowing you to require valid JWT bearer tokens before granting access to MCP servers. The `mcpAuthentication` policy handles token validation, exposes OAuth resource metadata, and optionally adapts non-spec-compliant identity providers.

## How it works

When `mcpAuthentication` is configured on a route:

1. The gateway exposes an OAuth protected resource metadata endpoint at `/.well-known/oauth-protected-resource/<path>`
2. Unauthenticated requests receive `401 Unauthorized` with a `WWW-Authenticate` header pointing to the metadata endpoint
3. MCP clients follow the OAuth flow to obtain a token from your authorization server
4. Requests with a valid bearer token are forwarded to the upstream MCP server

## Provider scenarios

<Tabs>
  <Tab title="Spec-compliant server">
    Use this configuration when your authorization server fully implements the MCP Authorization spec. Agentgateway acts as the resource server and validates tokens directly.

    ```yaml theme={null}
    policies:
      cors:
        allowHeaders:
        - mcp-protocol-version
        - content-type
        allowOrigins:
        - '*'
        exposeHeaders:
        - "Mcp-Session-Id"
      mcpAuthentication:
        mode: strict
        issuer: http://localhost:9000
        audiences:
        - http://localhost:3000/stdio/mcp
        jwks:
          url: http://localhost:9000/.well-known/jwks.json
        resourceMetadata:
          resource: http://localhost:3000/stdio/mcp
          scopesSupported:
          - read:all
          bearerMethodsSupported:
          - header
          - body
          - query
          resourceDocumentation: http://localhost:3000/stdio/docs
          resourcePolicyUri: http://localhost:3000/stdio/policies
    ```

    The route must also match the well-known metadata path:

    ```yaml theme={null}
    matches:
    - path:
        exact: /stdio/mcp
    - path:
        exact: /.well-known/oauth-protected-resource/stdio/mcp
    ```
  </Tab>

  <Tab title="Keycloak">
    When using Keycloak, set `provider.keycloak: {}` to enable the Keycloak adapter. The gateway exposes modified well-known endpoints and proxies client registration to Keycloak.

    ```yaml theme={null}
    policies:
      cors:
        allowHeaders:
        - mcp-protocol-version
        - content-type
        allowOrigins:
        - '*'
      mcpAuthentication:
        issuer: http://localhost:7080/realms/mcp
        audiences:
        - mcp_proxy
        jwks:
          url: http://localhost:7080/realms/mcp/protocol/openid-connect/certs
        provider:
          keycloak: {}
        resourceMetadata:
          resource: http://localhost:3000/keycloak/mcp
          scopesSupported:
          - profile
          - offline_access
          - openid
          bearerMethodsSupported:
          - header
          - body
          - query
    ```

    The route must match all Keycloak-specific paths:

    ```yaml theme={null}
    matches:
    - path: { exact: /keycloak/mcp }
    - path: { exact: /.well-known/oauth-protected-resource/keycloak/mcp }
    - path: { exact: /.well-known/oauth-authorization-server/keycloak/mcp }
    - path: { exact: /.well-known/oauth-authorization-server/keycloak/mcp/client-registration }
    - path: { exact: /realms/mcp/protocol/openid-connect/certs }
    ```

    <Note>
      Keycloak does not support RFC 8707 resource indicators. Use a fixed string for `audiences` rather than a resource URL.
    </Note>
  </Tab>

  <Tab title="Auth0">
    When using Auth0, set `provider.auth0: {}`. The gateway appends `?audience=...` to the authorization endpoint it exposes to clients.

    ```yaml theme={null}
    policies:
      cors:
        allowHeaders:
        - mcp-protocol-version
        - content-type
        allowOrigins:
        - '*'
      mcpAuthentication:
        issuer: https://dev-example.eu.auth0.com
        audiences:
        - urn:agent-gateway
        jwks:
          url: https://dev-example.eu.auth0.com/.well-known/jwks.json
        provider:
          auth0: {}
        resourceMetadata:
          resource: http://localhost:3000/auth0/mcp
          scopesSupported:
          - profile
          - offline_access
          - openid
          bearerMethodsSupported:
          - header
          - body
          - query
    ```

    If `jwks.url` is omitted, the gateway derives it automatically as `<issuer>/.well-known/jwks.json` for Auth0.
  </Tab>

  <Tab title="Remote MCP over HTTPS">
    You can also require authentication when proxying a remote MCP server over HTTPS. Add `backendTLS: {}` to enable TLS verification for the upstream connection.

    ```yaml theme={null}
    policies:
      backendTLS: {}
      cors:
        allowHeaders:
        - mcp-protocol-version
        - content-type
        allowOrigins:
        - '*'
        exposeHeaders:
        - "Mcp-Session-Id"
      mcpAuthentication:
        issuer: http://localhost:9000
        audiences:
        - http://localhost:3000/remote/mcp
        jwks:
          url: http://localhost:9000/.well-known/jwks.json
        resourceMetadata:
          resource: http://localhost:3000/remote/mcp
          scopesSupported:
          - offline_access
          bearerMethodsSupported:
          - header
          - body
          - query
    ```
  </Tab>
</Tabs>

## JWKS configuration

The gateway loads JSON Web Key Sets (JWKS) either from a URL or from a local file:

<CodeGroup>
  ```yaml JWKS from URL theme={null}
  mcpAuthentication:
    issuer: https://your-idp.example.com
    jwks:
      url: https://your-idp.example.com/.well-known/jwks.json
  ```

  ```yaml JWKS from file theme={null}
  mcpAuthentication:
    issuer: agentgateway.dev
    jwks:
      # Relative to the directory the binary runs from, not the config file
      file: ./manifests/jwt/pub-key
  ```
</CodeGroup>

## JWT validation options

By default, the `exp` (expiration) claim is required in every token. You can customize which RFC 7519 registered claims must be present using `jwtValidationOptions.requiredClaims`.

Only the following claim names are recognized: `exp`, `nbf`, `aud`, `iss`, `sub`. Any other value is silently ignored.

<Note>
  This setting only enforces **presence**. Standard claims like `exp` are always validated when present — an expired token is rejected regardless of `requiredClaims`.
</Note>

<CodeGroup>
  ```yaml Require no claims (allow tokens without exp) theme={null}
  mcpAuthentication:
    issuer: https://enterprise-idp.example.com
    audiences:
    - https://api.mycompany.com/mcp
    jwks:
      url: https://enterprise-idp.example.com/.well-known/jwks.json
    jwtValidationOptions:
      requiredClaims: []
  ```

  ```yaml Require both exp and nbf theme={null}
  mcpAuthentication:
    issuer: https://strict-idp.example.com
    audiences:
    - https://api.mycompany.com/mcp
    jwks:
      url: https://strict-idp.example.com/.well-known/jwks.json
    jwtValidationOptions:
      requiredClaims: ["exp", "nbf"]
  ```
</CodeGroup>

<Warning>
  Tokens without `exp` remain valid until the signing key is rotated. Only use `requiredClaims: []` when your identity provider intentionally omits expiration and you have a key rotation strategy in place.
</Warning>

## Running the authentication example

<Steps>
  <Step title="Start the demo dependencies">
    The authentication example uses Keycloak and a mock authorization server. Start them with:

    ```bash theme={null}
    make run-validation-deps
    ```

    This starts the mock authorization server on `http://localhost:9000` and Keycloak on `http://localhost:7080`.
  </Step>

  <Step title="Start the gateway">
    ```bash theme={null}
    cargo run -- -f examples/mcp-authentication/config.yaml
    ```
  </Step>

  <Step title="Test unauthenticated access">
    A request without a token should return `401 Unauthorized`:

    ```bash theme={null}
    curl -i http://localhost:3000/stdio/mcp
    ```

    The response includes a `WWW-Authenticate` header with a link to the resource metadata endpoint.
  </Step>

  <Step title="Test with MCP Inspector">
    ```bash theme={null}
    npx @modelcontextprotocol/inspector
    ```

    Set transport to **Streamable** and URL to `http://localhost:3000/stdio/mcp`. The MCP Authorization flow starts automatically after the initial `401` response. For Keycloak, use credentials `testuser` / `testpass`.
  </Step>
</Steps>

## What the provider adapter does

When you set a `provider`, the gateway acts as an Authorization Server facade for MCP clients:

* Exposes resource metadata at `/.well-known/oauth-protected-resource/...`
* Exposes authorization server metadata at `/.well-known/oauth-authorization-server/...` — pointing back to the gateway itself
* Fetches the real AS metadata from your `issuer` and rewrites it per-provider to smooth over protocol gaps
* Proxies client registration (Keycloak only) at `.../client-registration`

Omit the `provider` block entirely when your authorization server is already spec-compliant.

## Troubleshooting

* Ensure `issuer` matches the `iss` claim in your tokens exactly.
* Ensure each entry in `audiences` matches the `aud` claim clients request.
* Verify the resource metadata is reachable at `/.well-known/oauth-protected-resource/...` and that the `resource` value matches the `audiences` entry.
* Check that the JWKS URL is accessible from the gateway process.
