I Tried Obtaining an M2M Token Without a Domain Using Amazon Cognito's GetClientToken

I Tried Obtaining an M2M Token Without a Domain Using Amazon Cognito's GetClientToken

Amazon Cognito added a GetClientToken API that issues M2M access tokens. For M2M only, no user pool domain, certificate or DNS setup is needed. I tested token issuance and JWT signature verification with 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. With this API, you can obtain an access token for M2M from the AWS SDK, AWS CLI, or 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 Scopes, M2M, and resource servers explains 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 I Verified

Using the AWS CLI, I checked whether it is possible to complete everything from issuing an M2M access token to verifying the JWT signature without creating a single user pool domain. The AWS CLI I used was aws-cli/2.36.36, and the get-client-token subcommand was available. The region is ap-northeast-1.

I create a user pool, a resource server, and an app client dedicated to M2M, and then verify 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"

This is 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. After obtaining the token, I ran describe-user-pool again on the same pool, and the content was the same. I did not create a domain, an ACM certificate, or a DNS record.

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 is included in the access token obtained later.

App Client Dedicated to M2M

I created the app client that obtains the token. I specified ALLOW_CLIENT_TOKEN_AUTH as 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 together with user authentication flows. When I tried to create an app client specifying it together with ALLOW_USER_SRP_AUTH, 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 on an existing app client that had only ALLOW_USER_SRP_AUTH. When I tried to add ALLOW_CLIENT_TOKEN_AUTH afterward, the same error was returned.

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 an app client that has user authentication flows cannot be reused, you need to create a new app client dedicated to M2M.

Obtaining a Token

I obtained an access token with the created app client. To avoid leaving the client secret in command-line arguments and shell history, I passed a temporary file with --cli-input-json. Because I created this file after setting umask 077, its 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

I specified only three items: ClientId, Secret, and Scopes. I did not pass UserPoolId. The user pool is identified from the ClientId.

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

The token was contained in ClientAuthenticationResult, with a validity period of 3600 seconds and the token type Bearer.

JWT Signature Verification

I decoded the obtained access token and verified its signature with the public key from the 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 verification of the RS256 signature with that key succeeded. When I verified a JWT whose payload had been changed by just one character using the same key, it failed.

The token's iss was a URL composed of the region and the user pool ID. The JWKS URL was also under it, and the user pool domain appeared in neither. The format shown in the developer guide 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 side that verifies the token do not change depending on whether there is a domain.

No IAM Authorization

Calls to GetClientToken are not authorized by IAM. This is stated explicitly in the API reference 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 I called it with --no-sign-request, which omits request signing, a token was 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 I passed an incorrect client secret, 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 calls 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, I completed everything from issuing an M2M access token to verifying its signature without creating a single domain. The obtained token was a JWT signed with RS256. The user pool domain did not appear in the iss or the JWKS URL. Since both are in per-user-pool formats, no changes are needed to the issuer and JWKS settings on the side that verifies the token.

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

Even for existing configurations, if maintaining the domain is a burden and you can create a new app client dedicated to M2M and switch the callers, consider switching to GetClientToken.

Share this article

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