Auth0 doesn’t actually authenticate machine-to-machine APIs; it authorizes them by issuing tokens that grant specific permissions.

Let’s see this in action. Imagine a service, service-a, needs to call service-b. service-b is protected by Auth0.

First, service-a needs to get a token from Auth0. This isn’t a user logging in with a password. Instead, service-a uses its own credentials – a client ID and a client secret – to make a request to Auth0’s /oauth/token endpoint.

curl -X POST \
  https://your-tenant.auth0.com/oauth/token \
  -H 'content-type: application/json' \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "audience": "https://your-api-identifier.com",
    "grant_type": "client_credentials"
  }'

Auth0, upon verifying service-a’s credentials and checking its configuration (specifically, what scopes are allowed for this client), issues an Access Token. This token is a JSON Web Token (JWT).

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3lvdXItdGVuYW50LmF1dGgwLmNvbS8iLCJhdWQiOiJodHRwczovL3lvdXItYXBpLWlkZW50aWZpZXIuY29tIiwic3ViIjoiY2xpZW50X2lkX29mX3NlcnZpY2UtYSIsImF1dGhfYncmVnIjoiY2xpZW50Iiwic2NvcGUiOiJyZWFkOmRhdGEgd3JpdGU6ZGF0YSIsImlhdCI6MTcxNTM4MjcwMCwiZXhwIjoxNzE1Mzg2MzAwfQ.your_signature_here",
  "token_type": "Bearer",
  "expires_in": 3600
}

The aud (audience) claim in this token tells service-a which API this token is intended for. The scope claim specifies the permissions granted to service-a for service-b.

Now, service-a can call service-b by including this Access Token in the Authorization header:

curl -X GET \
  https://your-service-b.com/data \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

service-b receives the request and, critically, must validate the incoming token. It checks:

  1. Signature: Is the token signed by the expected issuer (Auth0)?
  2. Audience: Is the token intended for service-b?
  3. Expiration: Has the token expired?
  4. Scopes: Does the token contain the necessary scopes for the requested operation (e.g., read:data)?

This validation is typically done by using Auth0’s SDKs or by making a direct call to Auth0’s /oauth/jwks/ endpoint to fetch the public keys needed to verify the JWT signature.

The problem this solves is enabling trusted, secure communication between services without requiring human intervention or shared secrets managed outside of Auth0. Instead of hardcoding API keys or complex mutual TLS setups for every service interaction, you delegate the token issuance and validation to Auth0, which acts as a central authority. service-a doesn’t need to know service-b’s secrets, and service-b doesn’t need to know service-a’s secrets; they both trust Auth0.

The client_credentials grant type is specifically designed for this M2M scenario. It’s the OAuth 2.0 flow where a client application (like service-a) authenticates itself directly with the authorization server (Auth0) to obtain an access token, without the involvement of an end-user. The key is that the client itself is the one being authenticated, not a user on behalf of whom the client is acting.

When configuring your Auth0 API resource, you define the "Identifier." This is the value that will appear in the aud claim of the issued tokens. It’s crucial that this identifier is unique and matches what your protected API (service-b) expects. If service-b expects https://myapi.internal but Auth0 issues a token with aud: https://anotherapi.internal, the validation will fail.

The sub (subject) claim in the M2M token usually represents the client ID of the machine-to-machine application. This allows the protected API to know which client made the request, enabling fine-grained authorization beyond just scopes. For example, service-b could check if sub: client_id_of_service_a is allowed to access specific resources.

A common pitfall is forgetting to configure the correct "Allowed Audiences" for your M2M application in Auth0. If an M2M application is allowed to request tokens for audience https://service-a.com/api but it requests a token for https://service-b.com/api, Auth0 will deny the request with a 7001 error indicating an invalid audience. Conversely, if service-b is configured to accept tokens for https://service-b.com/api but service-a requests a token for https://service-a.com/api, service-b’s validation will fail because the aud claim doesn’t match what it’s expecting.

The next hurdle is managing token refresh and revocation efficiently for a large fleet of services.

Want structured learning?

Take the full Auth0 course →