Bearer tokens
Static tokens and JWTs in one definition - outgoing OAuth2 client credentials and inbound verification with JWKS.
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
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 |
| Client secret | The OAuth2 client secret |
| 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 |
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. |
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 |
Inbound REST channels
Assign a definition to a REST channel by selecting Bearer token/<name> in the channel's security dropdown. From then on, every request to that channel must present a valid token:
A request without a token, or with one that does not verify, receives a 401 response with the challenge header defined by RFC 6750:
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
Bearer token definitions can be members of API client security groups, next to Basic Auth and API key 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
Bearerchallenge.
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 and rate limiting, so a session initialized with one token cannot be reused by a caller whose token resolves to a different definition.
How gateways authenticate agents end to end, including the other credential types and what rejections log, is described under MCP gateway security.
JWT verification reference
Inbound JWTs are verified in this order - the first failing step rejects the token:
- The token is extracted from the
Authorizationheader. TheBearerprefix is case-insensitive, per RFC 6750. - 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
noneand HMAC algorithms, is rejected outright. A key ID (kid) must be present. - The signing key matching the key ID is looked up in the provider's JWKS document.
- The signature is verified and the standard claims are validated -
exp,issandaudare all required to be present. The issuer must equal the configured issuer and the audience must contain the configured audience. - 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 a flood of refetches. 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
Keycloak
- Create a realm, e.g.
prod, or use an existing one. - 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.
- 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.
- To filter by claims, add a User Attribute or Hardcoded claim mapper that puts e.g.
departmentinto the token, then setdepartment=Accountingin the definition's Claims. - The definition's fields:
- Auth endpoint -
https://keycloak.example.com/realms/prod/protocol/openid-connect/token - Issuer -
https://keycloak.example.com/realms/prod - JWKS URL -
https://keycloak.example.com/realms/prod/protocol/openid-connect/certs, or leave it empty to use discovery.
A test call:
$ 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:17010/api/orders
Microsoft Entra ID
- Register an application - App registrations > New registration - and create a client secret under Certificates and secrets.
- The token endpoint is
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token. - Set the definition's Issuer to
https://login.microsoftonline.com/<tenant-id>/v2.0and the Audience to the application ID URI or the client ID of the API being protected. - Scopes for the client credentials grant use the
.defaultsuffix, e.g.api://my-api/.default- put that in the definition's Scopes field. - 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
The same definition drives outgoing calls. When an outgoing REST connection 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.
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, 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.
OpenAPI console sign-in
A Bearer token definition also signs in to the OpenAPI console - 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.
Enmasse
Bearer token definitions are exported and imported with enmasse under the security key, with type: bearer_token. All the inbound fields round-trip.
security:
# A static definition
- name: partner.static
type: bearer_token
static_token: Zato_Enmasse_Env.Partner_Static_Token
# A JWT definition with inbound verification
- name: keycloak.accounting
type: bearer_token
username: my-client-id
password: Zato_Enmasse_Env.Keycloak_Client_Secret
auth_endpoint: https://keycloak.example.com/realms/prod/protocol/openid-connect/token
issuer: https://keycloak.example.com/realms/prod
jwks_url: https://keycloak.example.com/realms/prod/protocol/openid-connect/certs
audience: orders-api
claims:
- department=Accounting
groups:
- name: api.orders.clients
members:
- partner.static
- keycloak.accounting
mcp_gateway:
- name: orders.mcp
is_active: true
url_path: /mcp/orders
security_groups:
- api.orders.clients
channel_rest:
- name: api.orders
service: orders.get-list
url_path: /api/orders
security: keycloak.accounting
| Field | Notes |
|---|---|
| name | The definition's name |
| type | Always bearer_token |
| username | The OAuth2 client ID |
| password | The OAuth2 client secret |
| auth_endpoint | The token endpoint |
| static_token | The exact token of a static definition - a definition is static when this field is set |
| static_header | Header holding the static token, Authorization by default |
| static_prefix | Prefix before the static token, Bearer by default |
| issuer | Expected iss claim, defaults to the scheme and host of auth_endpoint |
| jwks_url | Where the signing keys live, defaults to the issuer's discovery document |
| audience | Expected aud claim, required for inbound JWT matching |
| claims | A list of name=value entries the token must include |
| data_format | Format of the outgoing token request |
| grant_type | The OAuth2 grant for outgoing tokens |
| scopes | Scopes requested with outgoing tokens |
| extra_fields | Additional name=value fields for the token request |
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 |