Docs

Authorize M2M Requests with Microsoft Entra ID (Azure AD)

Configure Apache APISIX to validate Microsoft Entra ID access tokens and authorize machine-to-machine requests with an application role.

Applies to:Apache APISIX 3.18.0

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

Goal

A route that validates Microsoft Entra ID machine-to-machine access tokens locally with the tenant's JSON Web Key Set (JWKS), requires the protected API's audience and access_as_application app role, and removes authentication headers before proxying upstream.

Before you begin

  • You have a Microsoft Entra tenant and permission to register applications and app roles.
  • You can grant tenant-wide admin consent: Cloud Application Administrator or Application Administrator is sufficient for this custom API; a Privileged Role Administrator is required when granting Microsoft Graph application permissions.
  • 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 entra-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/entra-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

Five steps register the protected API, define its application role, create the calling application, grant and capture its permission, and create the APISIX route.

Step 1: Register the protected API

In the Microsoft Entra admin center, select Entra ID → App registrations → New registration and configure:

  • Name: APISIX Protected API
  • Supported account types: accounts in this organizational directory only
  • Redirect URI: leave blank

After registration, copy the Application (client) ID and Directory (tenant) ID from the overview page.

Expected: App registrations lists the single-tenant APISIX Protected API application.

What changed: the tenant now has a separate application registration representing the API protected by APISIX.

Step 2: Define the application permission

On the protected API registration, select Expose an API, add and accept the generated api:// Application ID URI, then create an app role under App roles → Create app role:

  • Display name: Access APISIX API
  • Allowed member types: Applications
  • Value: access_as_application
  • Description: Access the API protected by APISIX
  • Enabled: yes

Select Manifest, find requestedAccessTokenVersion in the api object, set it to 2, and save.

Expected: the protected API exposes the enabled application role access_as_application, and its manifest contains "requestedAccessTokenVersion": 2.

What changed: Microsoft Entra ID can issue version 2 access tokens for this API whose roles claim contains the application permission.

Step 3: Register the M2M client

Create another single-tenant app registration named APISIX M2M Client with no redirect URI. Under Certificates & secrets → Client secrets, create a secret with an expiration that follows your credential-rotation policy.

Copy the client secret Value immediately. Microsoft Entra ID displays it only once; the Secret ID is not the credential. For production, prefer a certificate or workload identity federation.

Expected: the M2M registration has an application client ID and one unexpired credential.

What changed: the calling service has a separate confidential identity that can authenticate to the tenant without a user.

Step 4: Grant and capture the application permission

On the M2M registration, select API permissions → Add a permission → My APIs → APISIX Protected API → Application permissions, select Access APISIX API, and add it. Carefully review the single permission, then select Grant admin consent for the tenant.

If the protected API is absent from My APIs, add the current administrator as an owner of both registrations and retry.

Expected: the permission status says Granted for the current tenant. The automatically added Microsoft Graph User.Read delegated permission, if shown, is not used by this flow.

Copy the tenant and application values into the shell:

read -rp "Microsoft Entra tenant ID: " ENTRA_TENANT_ID && export ENTRA_TENANT_ID
read -rp "Protected API client ID: " ENTRA_API_CLIENT_ID && export ENTRA_API_CLIENT_ID
read -rp "M2M application client ID: " ENTRA_M2M_CLIENT_ID && export ENTRA_M2M_CLIENT_ID
read -rsp "M2M application client secret value: " ENTRA_M2M_CLIENT_SECRET && export ENTRA_M2M_CLIENT_SECRET && echo
export ENTRA_DISCOVERY="https://login.microsoftonline.com/${ENTRA_TENANT_ID}/v2.0/.well-known/openid-configuration"
export ENTRA_API_ID_URI="api://${ENTRA_API_CLIENT_ID}"

Confirm that the tenant-specific discovery document publishes a JWKS endpoint:

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

Expected: a Microsoft HTTPS URL containing the tenant ID.

What changed: the M2M service principal has tenant-wide consent for the protected API's single application role, and the shell holds the values used by APISIX and the token request.

Step 5: Create the protected route

Create a route that validates version 2 tokens locally and requires both the protected API audience and application role:

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/entra-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": "${ENTRA_API_CLIENT_ID}",
      "discovery": "${ENTRA_DISCOVERY}",
      "bearer_only": true,
      "use_jwks": true,
      "claim_validator": {
        "audience": {
          "required": true,
          "match_with_client_id": true
        }
      },
      "claim_schema": {
        "type": "object",
        "properties": {
          "roles": {
            "type": "array",
            "contains": {"const": "access_as_application"}
          }
        },
        "required": ["roles"]
      },
      "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": "entra-m2m", "uri": "/anything/m2m/*", "plugins": { "openid-connect": { "bearer_only": true, "use_jwks": true, … } }, … } }

What changed: /anything/m2m/* now requires a version 2 Microsoft Entra token for the protected API whose roles claim contains access_as_application, and bearer credentials are not forwarded upstream.

Validate

Request an app-only access token for the protected API's statically consented permissions:

ENTRA_ACCESS_TOKEN="$(
  curl -sS "https://login.microsoftonline.com/${ENTRA_TENANT_ID}/oauth2/v2.0/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "client_id=${ENTRA_M2M_CLIENT_ID}" \
    --data-urlencode "client_secret=${ENTRA_M2M_CLIENT_SECRET}" \
    --data-urlencode "scope=${ENTRA_API_ID_URI}/.default" \
    --data-urlencode "grant_type=client_credentials" | \
  jq -er '.access_token'
)" && export ENTRA_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 ${ENTRA_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 before revoking identity-provider access:

curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/entra-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 Microsoft Entra ID, open APISIX M2M Client → API permissions, revoke the tenant-wide admin consent for Access APISIX API, then delete the APISIX M2M Client registration. Delete the APISIX Protected API registration only when no other client uses its Application ID URI or app role.

Troubleshoot

SymptomCauseFix
The token endpoint returns invalid_client.The tenant ID, M2M client ID, or secret value is wrong or expired.Recopy the IDs, create a new credential if needed, and use the secret Value, not its ID.
APISIX returns 401 for a Microsoft-issued token.The token version, issuer, signature, or audience does not match the route.Confirm requestedAccessTokenVersion is 2, fetch ${ENTRA_DISCOVERY}, and verify the token's aud equals ${ENTRA_API_CLIENT_ID}.
APISIX rejects the token because its roles claim is missing or does not match.The application role was not granted as an application permission or tenant-wide consent is absent.Review the M2M app's API permissions, grant Access APISIX API, provide admin consent, and request a new token.