I tried getting an M2M token without a domain using Amazon Cognito's GetClientToken

I tried getting an M2M token without a domain using Amazon Cognito's GetClientToken

Amazon Cognito has added a GetClientToken API that issues access tokens for M2M. If your purpose is M2M only, you no longer need to create a user pool domain. There is no need to provision a domain, issue certificates, or configure DNS. I verified the entire flow from token issuance to JWT signature verification using the AWS CLI.
2026.09.02

This page has been translated by machine translation. View original

Introduction

On August 31, 2026, the GetClientToken API was added to Amazon Cognito user pools. Using this API, you can obtain access tokens for M2M from the AWS SDK, AWS CLI, and API without creating a user pool domain.

https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-cognito-get-client-token/

The developer guide's Scopes, M2M, and resource servers describes the difference between the token endpoint and GetClientToken as follows.

They differ in setup: the token endpoint requires a user pool domain and suits applications that use an OIDC library, while GetClientToken requires no domain and works through the AWS SDK, AWS CLI, or API.

What Was Verified

Using the AWS CLI, I confirmed whether it is possible to complete the entire process of issuing M2M access tokens and verifying JWT signatures without creating a single user pool domain. The AWS CLI used was aws-cli/2.36.36, and the get-client-token subcommand was available. The region is ap-northeast-1.

I created a user pool, a resource server, and a dedicated M2M app client, then verified the token obtained with GetClientToken.

Creating a Pool Without a Domain

I created a user pool without specifying a domain and added a resource server with a custom scope.

aws cognito-idp create-user-pool \
    --pool-name m2m-domainless-20260901151058

aws cognito-idp create-resource-server \
    --user-pool-id "$user_pool_id" \
    --identifier urn:example:m2m-domainless-20260901151058 \
    --name "M2M domainless resource server 20260901151058" \
    --scopes ScopeName=read,ScopeDescription="Read access"

The result of checking the created user pool with describe-user-pool (excerpt).

{
    "UserPool": {
        "Id": "ap-northeast-1_xxxxxxxxx",
        "Name": "m2m-domainless-20260901151058",
        "EstimatedNumberOfUsers": 0,
        "Arn": "arn:aws:cognito-idp:ap-northeast-1:123456789012:userpool/ap-northeast-1_xxxxxxxxx",
        "UserPoolTier": "ESSENTIALS",
        "KeyConfiguration": {
            "KeyType": "AWS_OWNED_KEY"
        },
        "IssuerConfiguration": {
            "Type": "ORIGINAL"
        }
    }
}

The response did not include a Domain field. Even after obtaining a token and running describe-user-pool again against the same pool, the content was the same. No domain, ACM certificate, or DNS record was created.

The response from creating the resource server is as follows.

{
    "ResourceServer": {
        "UserPoolId": "ap-northeast-1_xxxxxxxxx",
        "Identifier": "urn:example:m2m-domainless-20260901151058",
        "Name": "M2M domainless resource server 20260901151058",
        "Scopes": [
            {
                "ScopeName": "read",
                "ScopeDescription": "Read access"
            }
        ]
    }
}

The read scope defined here will be included in the access token obtained later.

Dedicated M2M App Client

I created an app client for obtaining tokens. I specified ALLOW_CLIENT_TOKEN_AUTH for the authentication flow and also generated a client secret. The scopes that can be specified when obtaining a token were registered with --allowed-o-auth-scopes.

aws cognito-idp create-user-pool-client \
    --user-pool-id "$user_pool_id" \
    --client-name m2m-client-20260901151058 \
    --generate-secret \
    --explicit-auth-flows ALLOW_CLIENT_TOKEN_AUTH \
    --allowed-o-auth-flows client_credentials \
    --allowed-o-auth-scopes urn:example:m2m-domainless-20260901151058/read \
    --allowed-o-auth-flows-user-pool-client
App client creation response (full text)
{
  "UserPoolClient": {
    "UserPoolId": "ap-northeast-1_xxxxxxxxx",
    "ClientName": "m2m-client-20260901151058",
    "ClientId": "<REDACTED>",
    "ClientSecret": "<REDACTED>",
    "RefreshTokenValidity": 30,
    "TokenValidityUnits": {},
    "ExplicitAuthFlows": [
      "ALLOW_CLIENT_TOKEN_AUTH"
    ],
    "AllowedOAuthFlows": [
      "client_credentials"
    ],
    "AllowedOAuthScopes": [
      "urn:example:m2m-domainless-20260901151058/read"
    ],
    "AllowedOAuthFlowsUserPoolClient": true,
    "EnableTokenRevocation": true,
    "EnablePropagateAdditionalUserContextData": false,
    "AuthSessionValidity": 3
  }
}

ALLOW_CLIENT_TOKEN_AUTH cannot be specified at the same time as user authentication flows. When attempting to create an app client with both ALLOW_USER_SRP_AUTH and this option, the following error was returned.

An error occurred (InvalidParameterException) when calling the CreateUserPoolClient operation: ALLOW_CLIENT_TOKEN_AUTH is not a permitted ExplicitAuthFlow when user auth-flows are enabled.

I ran update-user-pool-client against an existing app client that only had ALLOW_USER_SRP_AUTH. Attempting to add ALLOW_CLIENT_TOKEN_AUTH after the fact returned the same error.

An error occurred (InvalidParameterException) when calling the UpdateUserPoolClient operation: ALLOW_CLIENT_TOKEN_AUTH is not a permitted ExplicitAuthFlow when user auth-flows are enabled.

Since app clients with user authentication flows cannot be reused, it is necessary to create a new dedicated M2M app client.

Obtaining a Token

I obtained an access token using the created app client. To avoid leaving the client secret in command-line arguments and shell history, I passed it via a temporary file using --cli-input-json. Since I set umask 077 before creating this file, the permissions are 600.

umask 077
cat > request.json <<EOF
{
  "ClientId": "$client_id",
  "Secret": "$client_secret",
  "Scopes": ["urn:example:m2m-domainless-20260901151058/read"]
}
EOF

aws cognito-idp get-client-token --cli-input-json file://request.json

rm -f request.json

Only three values were specified: ClientId, Secret, and Scopes. UserPoolId was not passed. The user pool is identified from the ClientId.

{
  "ClientAuthenticationResult": {
    "AccessToken": "<REDACTED>",
    "ExpiresIn": 3600,
    "TokenType": "Bearer"
  }
}

The token is included in ClientAuthenticationResult, with a validity period of 3600 seconds and a token type of Bearer.

JWT Signature Verification

I decoded the obtained access token and verified the signature using the public key from JWKS. The output of the verification script is as follows.

{
  "algorithm": "RS256",
  "keyId": "<kid>",
  "claims": {
    "token_use": "access",
    "client_id": "<REDACTED>",
    "scope": "urn:example:m2m-domainless-20260901151058/read",
    "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
    "exp": 1788279066
  },
  "expiresInFromResponse": 3600,
  "jwksUrl": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>/.well-known/jwks.json",
  "signatureVerification": {
    "originalJwtRs256Valid": true,
    "tamperedPayloadJwtRs256Valid": false
  }
}

The JWKS contained a key matching the kid in the JWT header, and RS256 signature verification succeeded using that key. When I verified a JWT with one character changed in the payload using the same key, it failed.

The token's iss was a URL composed of the region and user pool ID. The JWKS URL was also under that path, and the user pool domain did not appear in either. The format shown in the developer guide's Verifying JSON web tokens is also per user pool.

You can find the JWKS URI for your user pool at https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json.

The issuer and JWKS settings on the token verification side do not change depending on whether a domain exists.

Absence of IAM Authorization

GetClientToken calls are not authorized by IAM. This is explicitly stated in the API reference's GetClientToken.

Amazon Cognito doesn't evaluate AWS Identity and Access Management (IAM) policies in requests for this API operation. For this operation, you can't use IAM credentials to authorize requests, and you can't grant IAM permissions in policies.

Even when calling with --no-sign-request, which omits request signing, a token was still issued. The exit status and response recorded during verification are as follows.

{
  "exitStatus": 0,
  "response": {
    "ClientAuthenticationResult": {
      "AccessToken": "<REDACTED>",
      "ExpiresIn": 3600,
      "TokenType": "Bearer"
    }
  },
  "stderr": ""
}

On the other hand, when an incorrect client secret was passed, no token was issued.

An error occurred (NotAuthorizedException) when calling the GetClientToken operation: Invalid client or secret

The only thing that authorizes requests to this API is the client secret, and the protection of the secret determines whether a token can be issued. As a means of restricting requests before they are accepted, the API reference mentions AWS WAF. The description of ForbiddenException is as follows.

This exception is thrown when AWS WAF doesn't allow your request based on a web ACL that's associated with your user pool.

Summary

With GetClientToken, the entire process from issuing M2M access tokens to verifying signatures was completed without creating a single domain. The token obtained was a JWT signed with RS256. The user pool domain did not appear in either the iss or JWKS URL. Since both use a per-user-pool format, no changes are needed to the issuer and JWKS settings on the token verification side.

If you are configuring M2M from scratch and do not plan to use a custom domain for purposes other than Cognito, you can take advantage of GetClientToken.

Even for existing configurations, if maintaining a domain is a concern and it is feasible to create a new dedicated M2M app client and switch the calling side, please consider switching to GetClientToken.

Share this article

AWSのお困り事はクラスメソッドへ