Docs

Authorize M2M Requests with Amazon Cognito

Configure Apache APISIX to validate Amazon Cognito access tokens and authorize machine-to-machine requests by client and custom scope.

Applies to:Apache APISIX 3.18.0

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

Goal

A route that validates Amazon Cognito machine-to-machine access tokens locally with the user pool's JSON Web Key Set (JWKS), requires the intended app client and https://apisix.test/read scope, and removes authentication headers before proxying upstream.

Before you begin

  • You have an AWS account with permission to manage Amazon Cognito user pools, resource servers, domains, and app clients.
  • You have an Apache APISIX installation, cURL, and jq. Get APISIX installs APISIX in Docker or Kubernetes.

Amazon Cognito charges for M2M token requests. Review Amazon Cognito pricing, reuse each token until shortly before it expires, and delete guide-specific resources when finished.

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 cognito-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/cognito-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

Four steps prepare a user pool, define the protected API scope, authorize an M2M app client, and create the APISIX route.

Step 1: Prepare a user pool and domain

In the AWS Management Console, open Amazon Cognito → User pools and reuse a suitable test pool with a domain, or create an isolated pool:

  1. Select Create user pool.
  2. Select Machine-to-machine application as the application type.
  3. Name the application APISIX M2M Client.
  4. Select Create user directory.

Cognito creates the pool, a confidential M2M app client, a user pool domain, and a generated resource server. The next steps use a guide-specific resource server rather than the generated one.

Expected: the pool overview shows a user pool ID, and Branding → Domain shows a domain for its OAuth endpoints.

What changed: a user pool with a token endpoint and confidential M2M app client is available for the guide.

Step 2: Create the protected resource scope

In the user pool, open Branding → Domain, locate Resource servers, select Create resource server, and configure:

  • Name: APISIX Protected API
  • Identifier: https://apisix.test
  • Custom scope name: read
  • Custom scope description: Read protected messages

Expected: the resource server lists the full custom scope https://apisix.test/read.

What changed: the user pool can issue access tokens carrying a custom scope for the API protected by APISIX.

Step 3: Authorize the M2M app client

If step 1 reused an existing pool, create a confidential app client under Applications → App clients → Create app client, choose Machine-to-machine application, name it APISIX M2M Client, and generate a client secret.

Open the app client, select Login pages → Edit, keep Client credentials as its only OAuth grant, add the custom scope https://apisix.test/read, remove any generated scope that this client does not need, and save.

Copy the pool, domain, and app-client values into the shell:

read -rp "Cognito AWS Region: " COGNITO_REGION && export COGNITO_REGION
read -rp "Cognito user pool ID: " COGNITO_USER_POOL_ID && export COGNITO_USER_POOL_ID
read -rp "Cognito domain hostname: " COGNITO_DOMAIN && export COGNITO_DOMAIN
read -rp "Cognito M2M client ID: " COGNITO_M2M_CLIENT_ID && export COGNITO_M2M_CLIENT_ID
read -rsp "Cognito M2M client secret: " COGNITO_M2M_CLIENT_SECRET && export COGNITO_M2M_CLIENT_SECRET && echo
export COGNITO_M2M_SCOPE="https://apisix.test/read"
export COGNITO_ISSUER="https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}"
export COGNITO_DISCOVERY="${COGNITO_ISSUER}/.well-known/openid-configuration"

Confirm that the discovery document publishes a JWKS endpoint:

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

Expected: an HTTPS URL under cognito-idp in the selected AWS Region.

What changed: the app client can request only the guide's custom scope, and the local shell holds the values needed by APISIX and the token request.

Step 4: Create the protected route

Create a route that validates Cognito access tokens locally and reads their client_id claim as the audience because Cognito Client Credentials tokens do not contain a resource-server aud claim:

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/cognito-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": "${COGNITO_M2M_CLIENT_ID}",
      "discovery": "${COGNITO_DISCOVERY}",
      "bearer_only": true,
      "use_jwks": true,
      "claim_validator": {
        "audience": {
          "claim": "client_id",
          "required": true,
          "match_with_client_id": true
        }
      },
      "required_scopes": ["${COGNITO_M2M_SCOPE}"],
      "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": "cognito-m2m", "uri": "/anything/m2m/*", "plugins": { "openid-connect": { "bearer_only": true, "use_jwks": true, … } }, … } }

What changed: /anything/m2m/* now requires a Cognito access token from the configured app client with https://apisix.test/read, and bearer credentials are not forwarded upstream.

Validate

Request one access token for the protected API, then reuse it for the remaining checks:

COGNITO_ACCESS_TOKEN="$(
  curl -sS "https://${COGNITO_DOMAIN}/oauth2/token" \
    --user "${COGNITO_M2M_CLIENT_ID}:${COGNITO_M2M_CLIENT_SECRET}" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "grant_type=client_credentials" \
    --data-urlencode "scope=${COGNITO_M2M_SCOPE}" | \
  jq -er '.access_token'
)" && export COGNITO_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 ${COGNITO_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 without creating another billable token response:

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

Roll back / Clean up

Delete the APISIX route before removing the Cognito resources it depends on:

curl -s -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/cognito-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.

If you reused a user pool, delete APISIX M2M Client under Applications → App clients, then delete APISIX Protected API from Resource servers under Branding → Domain, only when no other integration uses them. If step 1 created an isolated pool for this guide and it contains nothing else, delete the pool instead.

Troubleshoot

SymptomCauseFix
The token endpoint returns invalid_client or unauthorized_client.The app-client credentials are wrong, or Client Credentials is not its only enabled OAuth grant.Recopy the client ID and secret and review the app client's OAuth grant settings.
APISIX returns 401 for a Cognito-issued token.The issuer or signature is invalid, or the token's client_id does not match the configured app client.Fetch ${COGNITO_DISCOVERY} from the APISIX network and confirm the token was issued to ${COGNITO_M2M_CLIENT_ID}.
APISIX returns 403 for a valid token.The token lacks https://apisix.test/read.Add the custom scope to the app client, request it with scope=${COGNITO_M2M_SCOPE}, and reuse the new token until it nears expiry.