# Bearer tokens

Static tokens and JWTs in one definition - outgoing OAuth2 client credentials with a client secret or a private key JWT, and inbound verification with JWKS.

[Video: New Bearer token definition](https://zatosource-production.b-cdn.net/docs/gfx/security/bearer-token-create.webm?v=1790934309)

Dashboard menu: Security > Bearer tokens

## Overview {#overview}

A Bearer token security definition describes credentials sent in the HTTP `Authorization` header with the `Bearer` prefix. One definition serves both directions of traffic:

- **Outgoing** - Zato obtains a token from an identity provider using the OAuth2 client credentials grant and attaches it to outgoing REST calls, caching and refreshing it automatically.
- **Inbound** - Zato verifies tokens that callers present when they invoke REST channels or MCP gateways.

There are two kinds of tokens:

- **Static tokens** - a constant string that you configure once and that callers must present exactly. Verification is a constant-time comparison, there is no identity provider involved at all.
- **JWTs** - tokens issued by an identity provider such as Keycloak or Microsoft Entra ID. Zato validates them locally against the provider's published JWKS keys - the provider is never called during request processing, so verification adds no per-request network traffic.

Definitions are managed in the dashboard under **Security** > **Bearer tokens**. The create and edit forms have two tabs - **Dynamic** for definitions backed by an identity provider and **Static** for constant tokens. Changes take effect immediately, without server restarts.

## Definition fields {#definition-fields}

Fields of a dynamic definition:

| Field | Notes |
| --- | --- |
| Name | A unique name for the definition |
| Client ID | The OAuth2 client ID, used when obtaining outgoing tokens |
| Authentication | How Zato proves the client's identity to the token endpoint - **Client secret** or **Private key JWT**. The rows below the select change with it. |
| Client secret | The OAuth2 client secret, with the client secret method |
| Private key | With the private key JWT method, the PEM private key that signs each token request. Its public half is what you register with the identity provider, and the key is stored encrypted and never shown again after saving. |
| Algorithm | The signing algorithm of the private key JWT method - `RS384` by default, or `RS256`, `PS256` and `ES384`. The RS and PS algorithms take an RSA key, `ES384` takes an EC P-384 key. |
| Key ID | Optional `kid` sent in the header of each signed request, for providers that pick the verification key by its ID. |
| Assertion audience | Optional override of the `aud` claim of the signed request, which is the auth endpoint's URL unless the provider says otherwise. |
| Certificate | Optional PEM certificate of the private key. With it set, the `x5t` and `x5t#S256` thumbprint headers are sent, which Microsoft Entra ID uses to find the key. |
| Auth endpoint | The identity provider's token endpoint, e.g. `https://idp.example.com/realms/prod/protocol/openid-connect/token` |
| Data format | Format of the token request, JSON or form data |
| Grant type | The OAuth2 grant, `client_credentials` by default |
| Scopes | Optional scopes requested with each outgoing token |
| Extra fields | Optional additional `name=value` fields for the token request |

The assertion audience and the certificate are collapsed behind a **Toggle options** link in the private key rows, since most providers need neither.

Fields used for inbound verification, in the same form under **Channel verification**, collapsed behind a **Toggle options** link:

| Field | Notes |
| --- | --- |
| Issuer | The expected `iss` claim. Defaults to the scheme and host of the auth endpoint. |
| JWKS URL | Where the provider publishes its signing keys. Defaults to the issuer's `/.well-known/openid-configuration` discovery document, whose `jwks_uri` is followed automatically. |
| Audience | The expected `aud` claim. **Required for inbound JWT verification** - a definition without an audience never matches inbound traffic. |
| Claims | Optional `name=value` pairs, one per line, that the token must include in addition to the standard claims. |
| Identity claim | Optional name of the claim that identifies the person or application behind a token, e.g. `preferred_username`. With it set, a caller's identity is the definition's name, a slash and the claim's value, `orders.agents/maria.johnson@example.com`, rather than the definition's name alone. |

Fields of a static definition:

| Field | Notes |
| --- | --- |
| Header | The HTTP header holding the token, `Authorization` by default |
| Prefix | The prefix before the token itself, `Bearer` by default |
| Token | The exact token value that callers must present |

The definition is exported and imported with [enmasse](https://zato.io/docs/admin/enmasse.html) under the `security` key, with `type: bearer_token` - the fields, both the outgoing and the inbound ones, are in the [enmasse reference](https://zato.io/docs/admin/enmasse-reference.html).

## Inbound REST channels {#inbound-rest-channels}

Assign a definition to a [REST channel](https://zato.io/docs/dev/rest/channels.html) by selecting **Bearer token/\<name>** in the channel's security dropdown. From then on, every request to that channel must present a valid token:

```bash
$ curl -H "Authorization: Bearer $TOKEN" http://localhost:11223/api/orders
```

A request without a token, or with one that does not verify, receives a 401 response with the challenge header defined by RFC 6750:

```text
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
```

Edits to the definition propagate to running channels immediately - for instance, changing the audience makes previously accepted tokens fail on the very next request, without a restart.

## Security groups and MCP gateways {#security-groups-and-mcp-gateways}

Bearer token definitions can be members of API client [security groups](https://zato.io/docs/security/groups.html), next to [Basic Auth](https://zato.io/docs/security/basic-auth.html) and [API key](https://zato.io/docs/security/api-keys.html) members. A group assigned to a channel accepts a request when any of its members matches the presented credentials.

- Groups are managed under **Security** > **Groups** in the dashboard.
- Each MCP gateway automatically creates its own group named `mcp.<gateway-name>` - add Bearer token definitions to that group to control which tokens may call the gateway.
- Group membership changes are live - adding or removing a member is reflected in enforcement immediately, without restarts.
- Rejections at the group level return 403, unlike direct channel assignments which return 401 with the `Bearer` challenge.

When a JWT matches a claim-filtered definition in a group, the identity that gets resolved is that specific definition - two definitions with different claim filters on one gateway resolve to different identities. The resolved identity flows into MCP sessions, [audit entries](https://zato.io/docs/ai/mcp/audit-log.html) and [rate limiting](https://zato.io/docs/ai/mcp/rate-limits.html), so a session initialized with one token cannot be reused by a caller whose token resolves to a different definition.

A definition with an identity claim resolves each token to the definition's name and the claim's value together, e.g. `orders.agents/maria.johnson@example.com`, and the same three places then tell people apart within one definition - a session belongs to the person who opened it, each person has rate-limit counters of their own and the audit log names the person. A token without the claim resolves to the definition's name alone, as before. This is what [OAuth for MCP gateways](https://zato.io/docs/ai/mcp/oauth.html) builds on, with people signing in through the identity provider from inside their AI clients.

How gateways authenticate agents end to end, including the other credential types and what rejections log, is described under [MCP gateway security](https://zato.io/docs/ai/mcp/security.html).

## JWT verification reference {#jwt-verification-reference}

Inbound JWTs are verified in this order - the first failing step rejects the token:

1. The token is extracted from the `Authorization` header. The `Bearer` prefix is case-insensitive, per RFC 6750.
2. The token's header is read without verification to learn the algorithm and the key ID. The only accepted signature algorithm is **RS256** - anything else, including `none` and HMAC algorithms, is rejected outright. A key ID (`kid`) must be present.
3. The signing key matching the key ID is looked up in the provider's JWKS document.
4. The signature is verified and the standard claims are validated - `exp`, `iss` and `aud` are all required to be present. The issuer must equal the configured issuer and the audience must contain the configured audience.
5. The configured claims are matched last. A scalar claim matches by equality, a list-valued claim, such as `groups`, matches by membership - the configured value must be one of the list's elements.

JWKS documents are cached for one hour. When a token arrives with a key ID that is not in the cached document, the document is refetched once - this is how identity provider key rotation is handled without restarts and without refetching the document for every token. Failed fetches are never cached, so a temporary provider outage does not lock callers out for the duration of the TTL.

If the JWKS URL points to an OpenID Connect discovery document rather than the keys themselves, its `jwks_uri` field is followed automatically - configuring the issuer alone is therefore enough for providers that publish a standard discovery document.

## Identity provider walkthroughs {#identity-provider-walkthroughs}

### Keycloak {#keycloak}

1. Create a realm, e.g. `prod`, or use an existing one.
2. Create a confidential client - **Clients** > **Create client**, enable **Client authentication** and **Service accounts roles**. The client ID and the secret from the **Credentials** tab go into the definition's Client ID and Client secret fields.
3. Keycloak does not add an audience by default - create an audience mapper. In the client's **Client scopes**, open the dedicated scope, add a mapper of type **Audience** and set **Included Custom Audience** to the value you configure as the definition's Audience.
4. To filter by claims, add a **User Attribute** or **Hardcoded claim** mapper that puts e.g. `department` into the token, then set `department=Accounting` in the definition's Claims.
5. The definition's fields:
6. Auth endpoint - `https://keycloak.example.com/realms/prod/protocol/openid-connect/token`
7. Issuer - `https://keycloak.example.com/realms/prod`
8. JWKS URL - `https://keycloak.example.com/realms/prod/protocol/openid-connect/certs`, or leave it empty to use discovery.

A test call:

```bash
$ TOKEN=$(curl -s -d grant_type=client_credentials \
    -d client_id=my-client -d client_secret=my-secret \
    https://keycloak.example.com/realms/prod/protocol/openid-connect/token | jq -r .access_token)

$ curl -H "Authorization: Bearer $TOKEN" http://localhost:11223/api/orders
```

#### Keycloak with a signed JWT {#keycloak-with-a-signed-jwt}

For outgoing calls, the same client can log in with a private key instead of the secret, which Keycloak calls the **Signed JWT** client authenticator:

1. Create the definition with **Authentication** set to **Private key JWT**, paste the private key, keep `RS384` or pick another algorithm and set a Key ID, e.g. `orders-key-1`.
2. Save the definition, open it again and follow the **Public key** link in the private key rows - it serves the public half of the key as PEM and as a JWK set.
3. In Keycloak, open the client, set **Client authenticator** on the **Credentials** tab to **Signed JWT**, pick the same algorithm and import the public key on the **Keys** tab, either as PEM or as the JWK set.
4. The **Get token** link in the definition's row confirms that Keycloak accepts the signed request.

### Microsoft Entra ID {#microsoft-entra-id}

1. Register an application - **App registrations** > **New registration** - and create a client secret under **Certificates and secrets**.
2. The token endpoint is `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token`.
3. Set the definition's Issuer to `https://login.microsoftonline.com/<tenant-id>/v2.0` and the Audience to the application ID URI or the client ID of the API being protected.
4. Scopes for the client credentials grant use the `.default` suffix, e.g. `api://my-api/.default` - put that in the definition's Scopes field.
5. To filter by claims, assign app roles to the calling application and match them with a list-valued claim, e.g. `roles=Orders.Read`.

## Outgoing bearer tokens {#outgoing-bearer-tokens}

The same definition drives outgoing calls. When an [outgoing REST connection](https://zato.io/docs/dev/rest/outconns.html) or an [outgoing FHIR connection](https://zato.io/docs/dev/healthcare/hl7/fhir/security/index.html) uses a Bearer token definition, Zato requests a token from the auth endpoint using the client credentials grant, attaches it as `Authorization: Bearer ...` and caches it, refreshing it before expiry. The grant type, scopes, data format and any extra fields of the token request all come from the definition.

The definition's **Authentication** field selects how Zato proves the client's identity in that token request:

- **Client secret** - the client ID and the client secret go in the request as `client_id` and `client_secret` form fields, or under the names set in the Client ID field and Client secret field options.
- **Private key JWT** - no secret is sent at all. Zato signs a short-lived JWT, a client assertion as defined in RFC 7523, with the definition's private key and sends it as `client_assertion`, together with `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. The assertion names the client ID as its issuer and subject, the auth endpoint as its audience, expires after five minutes and has a unique `jti`, so a captured assertion cannot be replayed. The provider verifies the signature with the public key you registered with it.

The private key never leaves the server - it is stored encrypted, the dashboard and enmasse exports never return it and the only thing a provider ever receives is a signature. Providers describe the same method under their own names, which the next section lists.

Static definitions work for outgoing traffic too - the configured token is attached as-is, with the configured header and prefix, which covers APIs that use fixed API tokens with Bearer semantics.

In connection types you build yourself with the [Connector SDK](https://zato.io/docs/sdk/reference.html), expired tokens follow the same idea in your own code - an invocation raises `CredentialsExpired`, the platform calls the connector's `refresh_credentials` and the invocation is retried once.

### Private key JWT by provider {#private-key-jwt-by-provider}

Each provider has a term of its own for a client that logs in with a signed JWT instead of a secret, and a place of its own where the public key is registered. In every case, the definition's Client ID is the provider's client ID, the Auth endpoint is the provider's token endpoint and the Private key is the key whose public half the provider holds.

**Okta** calls it **Public key / Private key** client authentication. Create an API Services app, choose that authentication method and add the public key under **Public keys** - the JWK set the definition's Public key link serves can be pasted as is. Set the definition's Key ID to the `kid` Okta shows for the key and keep the audience as the token endpoint, as Okta requires. Leave **Require DPoP** switched off, since the token request is a plain client credentials one.

**Auth0** calls it **Private Key JWT** and configures it under the application's **Credentials** tab. Auth0 verifies the assertion's audience against the tenant's base URL with a trailing slash, so set the definition's Assertion audience to `https://<tenant>.auth0.com/` and keep the Auth endpoint as `https://<tenant>.auth0.com/oauth/token`.

**Keycloak** calls it the **Signed JWT** client authenticator, with the public key or certificate imported on the client's **Keys** tab, as the [walkthrough above](https://zato.io/docs/security/bearer-tokens/index.html#keycloak-with-a-signed-jwt) shows.

**Microsoft Entra ID** calls it **certificate credentials** - the app registration holds a certificate rather than a bare public key, under **Certificates and secrets**. Fill in the definition's Certificate field with that certificate, so the request includes the thumbprint headers Entra ID uses to find it, and pick `PS256` or `RS256` as the algorithm. The Auth endpoint is `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token`.

**Ping Identity and ForgeRock** call it `private_key_jwt`, the token endpoint authentication method of a client on PingFederate, PingOne and PingAM. Register the public key or the JWKS on the client and set the client's signing algorithm to the one the definition uses.

**SMART Backend Services** is the term certified EHRs use for the same login - Epic, Oracle Health, eClinicalWorks and the other systems that expose FHIR APIs to backend applications. The token URL comes from the server's `.well-known/smart-configuration` document, the scopes are the `system/` ones, e.g. `system/Patient.read`, and the public key is registered with the EHR as a JWK set. The [FHIR security page](https://zato.io/docs/dev/healthcare/hl7/fhir/security/index.html) describes the setup in detail.

The method is also what FAPI and Open Banking profiles call for when they rule out client secrets - the same definition works with any provider that follows them.

## OpenAPI console sign-in {#openapi-console-sign-in}

A Bearer token definition also signs in to the [OpenAPI console](https://zato.io/docs/dev/rest/openapi-console.html) - the client ID is the username and the client secret is the password. The signed-in caller receives an OpenAPI document containing only the channels the definition is assigned to, directly or through group membership, and the console's try-it client enforces the same access rules.

## Troubleshooting {#troubleshooting}

Every rejection is logged in the server log with the definition's name, the channel and the correlation ID (cid). The messages below map each log entry to its cause.

| Log message | Cause |
| --- | --- |
| `Invalid static bearer token` | The presented token is not equal to the definition's static token |
| `Bearer token definition ... is not configured for inbound use` | The definition has neither a static token nor an audience - set the audience to enable inbound JWT matching |
| `Bearer token is not a valid JWT (...)` | The token is malformed - not three base64 segments |
| `Bearer token uses unsupported algorithm ...` | The token's `alg` header is not RS256 |
| `Bearer token has no key ID` | The token's header lacks a `kid` field |
| `No JWKS key matches key ID ... from ...` | No published key matches the token's `kid`, even after a refetch - the token comes from a different provider or the JWKS URL is wrong |
| `Bearer token has expired` | The `exp` claim is in the past |
| `Bearer token has a wrong audience, expected ...` | The `aud` claim does not contain the configured audience |
| `Bearer token has a wrong issuer, expected ...` | The `iss` claim does not equal the configured issuer |
| `Bearer token has an invalid signature` | The signature does not verify against the JWKS key - the token was tampered with or signed by a different key |
| `Bearer token is invalid (...)` | Another validation error, e.g. a required claim (`exp`, `iss`, `aud`) is missing from the token entirely |
| `Bearer token is missing claim ...` | A configured claim is not present in the token |
| `Bearer token claim ... is ... instead of ...` | A configured scalar claim has a different value |
| `Bearer token claim ... does not contain ...` | A configured value is not among the elements of a list-valued claim |
| `JWKS document could not be fetched from ...` | The JWKS URL did not respond with success - check connectivity and the URL |
| `Document from ... is not a JWKS document` | The URL responded but the payload has no `keys` field - the URL points at something other than a JWKS or discovery document |
| `Invalid bearer token (groups)` | A token was presented to a group-protected channel but no group member matched it - the caller receives 403 |
| `Received neither Basic Auth, bearer token nor API key (groups)` | An anonymous request reached a group-protected channel - the caller receives 403 |
| `401 Unauthorized path_info ... (Bearer token)` | A request to a channel with a directly assigned definition was rejected - the caller receives 401 with the `WWW-Authenticate: Bearer` challenge |

Errors of the private key JWT method are reported when the definition is saved, in the dashboard or by enmasse, or when a token is requested:

| Message | Cause |
| --- | --- |
| `Bearer token definition ... needs a private key to use private key JWT` | The authentication method is private key JWT but no key was given and none is stored yet |
| `Private key could not be parsed as PEM -> ...` | The pasted text is not a PEM private key - it must start with `-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----` or `-----BEGIN EC PRIVATE KEY-----` and must not be password-protected |
| `Algorithm ... needs an RSA private key, found ...` | `RS256`, `RS384` or `PS256` was picked for an EC key |
| `Algorithm ... needs an EC private key, found ...` | `ES384` was picked for an RSA key |
| `Algorithm ES384 needs a P-384 key, found curve ...` | The EC key is on a different curve - generate a P-384 one |
| `Certificate does not match the private key` | The certificate in the Certificate field was issued for a different key |
| `Bearer token for ... could not be obtained from ... -> 400 -> {"error":"invalid_client" ...}` | The provider rejected the signed request - the public key registered with it does not match the private key, the Key ID does not match the registered `kid`, the algorithm is not the one the client is configured with or the assertion audience is not what the provider expects |
| `Bearer token for ... could not be obtained from ... -> 400 -> {"error":"invalid_grant" ...}` | The provider accepted the client but not the request - with Auth0 and Okta, check that the assertion audience and the scopes are the ones the application allows |
| `Kafka connection ... cannot use bearer token definition ... because it authenticates with a private key JWT` | Kafka clients log in with a client secret only - use a client secret definition for Kafka connections |

## Learn more {#learn-more}

- [Securing channels](https://zato.io/docs/admin/security/channel.html) - Which security types a channel accepts, and how to assign them
- [Securing outgoing connections](https://zato.io/docs/admin/security/outgoing.html) - Credentials Zato presents to the systems it calls
- [API keys](https://zato.io/docs/security/api-keys.html) - Per-client keys in a configurable HTTP header
- [Basic Auth](https://zato.io/docs/security/basic-auth.html) - Usernames and passwords for channels and outgoing connections
- [Security groups](https://zato.io/docs/security/groups.html) - Many API client credentials on one channel or MCP gateway
- [Kerberos (SPNEGO)](https://zato.io/docs/security/kerberos/) - Negotiate authentication from a keytab, with no ticket management
- [mTLS](https://zato.io/docs/security/mtls/) - Client certificates in both directions, matched by fingerprint or DN
- [NTLM](https://zato.io/docs/security/ntlm.html) - Windows domain credentials for outgoing connections
- [WS-Security](https://zato.io/docs/security/ws-security.html) - UsernameToken, X.509 and SAML for SOAP traffic
- [Quota tiers](https://zato.io/docs/security/quota-tiers.html) - Named rate-limit plans assigned to any number of API clients
- [TLS certificates](https://zato.io/docs/admin/security/ssl/) - Your own certificates, the self-signed ones and how to rotate them
