Docs

Authorize M2M Requests with Auth0

Configure Apache APISIX to validate Auth0 access tokens and authorize machine-to-machine requests by audience and scope.

Applies to:Apache APISIX 3.18.0

Roles:Platform engineerSecurity engineerTime:20 minutesImpact:No restart, no traffic change

Goal

A route that validates Auth0 machine-to-machine access tokens locally with Auth0's JSON Web Key Set (JWKS), enforces their issuer, audience, and read:messages scope, and removes authentication headers before proxying requests upstream.

Before you begin

  • You have administrator access to an Auth0 tenant.
  • You have an Apache APISIX installation, cURL, and jq. Get APISIX installs APISIX in Docker or Kubernetes.

Set the APISIX variables this guide uses:

export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Admin API key: " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo
  • The Admin API accepts your key. Check it with:

    curl -s -o /dev/null -w '%{http_code}\n' "${APISIX_ADMIN_URL}/apisix/admin/routes" \
      -H "X-API-KEY: ${APISIX_ADMIN_KEY}"

    Expected: 200.

  • The data plane is reachable. Check it with:

    curl -sI "${GATEWAY_URL}" | grep -i '^server'

    Expected: a Server: APISIX/3.18.0 header.

  • The route ID auth0-m2m and path /anything/m2m/* are unused. Check both with:

    curl -s -o /dev/null -w '%{http_code}\n' \
      "${APISIX_ADMIN_URL}/apisix/admin/routes/auth0-m2m" \
      -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
    
    curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes" \
      -H "X-API-KEY: ${APISIX_ADMIN_KEY}" | \
      jq -r '.list[] | select(.value.uri == "/anything/m2m/*") | .key'

    Expected: 404 from the first command and no output from the second. A 200 or listed key means a collision; stop and choose a unique route ID and path rather than overwriting or shadowing an existing route.

Steps

Three steps register the protected API, authorize an M2M application, and create the APISIX route.

Step 1: Register the protected API

In the Auth0 Dashboard, select Applications → APIs → Create API, then configure:

  • Name: APISIX Protected API
  • Identifier: https://apisix.test
  • Access Token Profile: Auth0
  • Signing Algorithm: RS256

After creating the API, add the permission read:messages with the description Read protected messages in its permissions or scopes settings.

Expected: the API overview shows identifier https://apisix.test, signing algorithm RS256, and permission read:messages.

What changed: Auth0 now has a resource-server definition whose identifier becomes the access-token audience and whose permission becomes the required scope.

Step 2: Authorize an M2M application

Select Applications → Applications → Create Application, name it APISIX M2M Client, choose Machine to Machine Applications, and authorize it for APISIX Protected API with only read:messages.

From the application's settings, copy its domain, client ID, and client secret into shell variables:

read -rp "Auth0 tenant domain: " AUTH0_DOMAIN && export AUTH0_DOMAIN
read -rp "Auth0 M2M client ID: " AUTH0_M2M_CLIENT_ID && export AUTH0_M2M_CLIENT_ID
read -rsp "Auth0 M2M client secret: " AUTH0_M2M_CLIENT_SECRET && export AUTH0_M2M_CLIENT_SECRET && echo
export AUTH0_API_AUDIENCE="https://apisix.test"
export AUTH0_DISCOVERY="https://${AUTH0_DOMAIN}/.well-known/openid-configuration"

Confirm that the discovery document publishes a JWKS endpoint:

curl -sS "${AUTH0_DISCOVERY}" | jq -er '.jwks_uri'

Expected: an HTTPS URL under your Auth0 tenant domain.

What changed: the M2M application can now request tokens for this API with the read:messages scope, and the local shell holds the values needed by APISIX.

Step 3: Create the protected route

Create a route that validates bearer tokens locally, requires the configured audience and scope, and strips authentication data before proxying:

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/auth0-m2m" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary @- <<EOF
{
  "uri": "/anything/m2m/*",
  "plugins": {
    "openid-connect": {
      "client_id": "${AUTH0_API_AUDIENCE}",
      "discovery": "${AUTH0_DISCOVERY}",
      "bearer_only": true,
      "use_jwks": true,
      "claim_validator": {
        "audience": {
          "required": true,
          "match_with_client_id": true
        }
      },
      "required_scopes": ["read:messages"],
      "set_access_token_header": false,
      "set_id_token_header": false,
      "set_userinfo_header": false
    },
    "proxy-rewrite": {
      "headers": {
        "remove": ["Authorization"]
      }
    }
  },
  "upstream": {
    "type": "roundrobin",
    "nodes": {"httpbin.org:80": 1}
  }
}
EOF
{ "value": { "id": "auth0-m2m", "uri": "/anything/m2m/*", "plugins": { "openid-connect": { "bearer_only": true, "use_jwks": true, … } }, … } }

What changed: /anything/m2m/* now requires an Auth0 token for the protected API with the read:messages scope, and bearer credentials are not forwarded upstream.

Validate

Request an access token with the Client Credentials flow:

AUTH0_ACCESS_TOKEN="$(
  curl -sS "https://${AUTH0_DOMAIN}/oauth/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "client_id=${AUTH0_M2M_CLIENT_ID}" \
    --data-urlencode "client_secret=${AUTH0_M2M_CLIENT_SECRET}" \
    --data-urlencode "audience=${AUTH0_API_AUDIENCE}" \
    --data-urlencode "scope=read:messages" \
    --data-urlencode "grant_type=client_credentials" | \
  jq -er '.access_token'
)" && export AUTH0_ACCESS_TOKEN

Send the token to APISIX and verify that httpbin did not receive authentication headers:

curl -sS "${GATEWAY_URL}/anything/m2m/get" \
  -H "Authorization: Bearer ${AUTH0_ACCESS_TOKEN}" | \
  jq '{authorization_forwarded: (.headers.Authorization != null), access_token_forwarded: (.headers["X-Access-Token"] != null), id_token_forwarded: (.headers["X-Id-Token"] != null), userinfo_forwarded: (.headers["X-Userinfo"] != null)}'
{
  "authorization_forwarded": false,
  "access_token_forwarded": false,
  "id_token_forwarded": false,
  "userinfo_forwarded": false
}

A request without a token is rejected:

curl -s -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/anything/m2m/get"
401

Roll back / Clean up

Delete the APISIX route, then remove the guide-specific M2M application and API in Auth0:

curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/auth0-m2m" \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}"

Expected: the Admin API returns a successful deletion response. The protected path stops matching this route immediately.

In the Auth0 Dashboard, delete APISIX M2M Client under Applications → Applications, then delete APISIX Protected API under Applications → APIs. Do not delete either resource if it was shared with another integration.

Troubleshoot

SymptomCauseFix
The token request returns 401 or access_denied.The application credentials are wrong, or the application has no client grant for this API.Recopy the client ID and secret, then confirm the application's API access grants read:messages for APISIX Protected API.
APISIX returns 401 for a token Auth0 issued.The discovery URL is unreachable from APISIX, the JWT signature is invalid, or its audience does not match client_id.Fetch ${AUTH0_DISCOVERY} from the APISIX network and confirm the token request used ${AUTH0_API_AUDIENCE}.
APISIX returns 403 for a valid token.The token does not contain the required read:messages scope.Grant only the needed permission to the M2M application, request a new token, and retry.