Get M2M client tokens and discover Terms with Amazon Cognito using an AWS SDK
The following code example shows how to use Amazon Cognito machine-to-machine (M2M) authorization and Terms discovery.
Deploy a CloudFormation stack with Cognito resources (user pool, resource server, app client, Terms documents).
Retrieve the app client secret with DescribeUserPoolClient.
Obtain M2M access tokens with GetClientToken.
Discover Terms documents with DescribeTermsByClient.
Clean up by deleting the CloudFormation stack.
- Python
-
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Run an interactive scenario at a command prompt.
class ClientTokenAndTermsScenario: """ Runs an interactive scenario that demonstrates the Amazon Cognito GetClientToken and DescribeTermsByClient APIs. """ def __init__( self, cognito_wrapper: CognitoIdentityProviderWrapper, cfn_client: BaseClient, ): """ :param cognito_wrapper: An instance of the CognitoIdentityProviderWrapper class. :param cfn_client: A Boto3 CloudFormation client. """ self.cognito_wrapper = cognito_wrapper self.cfn_client = cfn_client self.stack_name = None self.user_pool_id = None self.client_id = None self.client_secret = None def run(self) -> None: """Runs the scenario from start to finish.""" print(SEPARATOR) print("Welcome to the Amazon Cognito Client Token and Terms Scenario.") print(SEPARATOR) print("This example creates Cognito resources in a CloudFormation stack, then") print("demonstrates the new GetClientToken and DescribeTermsByClient APIs.") print() try: self.setup() self.get_client_secret() self.demonstrate_get_client_token() self.demonstrate_describe_terms() except Exception: logger.exception("An error occurred during the scenario.") finally: self.cleanup() print(SEPARATOR) print("Amazon Cognito Client Token and Terms scenario completed.") # ------------------------------------------------------------------ # Phase 1: Setup - deploy CloudFormation stack # ------------------------------------------------------------------ def setup(self) -> None: """Deploys the CloudFormation prerequisite stack and retrieves outputs.""" stack_name = q.ask( f"\nEnter a name for the CloudFormation stack [{DEFAULT_STACK_NAME}]: " ) if not stack_name: stack_name = DEFAULT_STACK_NAME # Check whether the stack name already exists. while self._stack_exists(stack_name): print(f"A stack named '{stack_name}' already exists.") stack_name = q.ask("Enter a different stack name: ") self.stack_name = stack_name template_body = self._read_template() print(f"Deploying CloudFormation stack: {self.stack_name}") self.cfn_client.create_stack( StackName=self.stack_name, TemplateBody=template_body, Capabilities=["CAPABILITY_IAM"], ) print(f"CloudFormation stack creation started: {self.stack_name}") print("Waiting for CloudFormation stack creation to complete...") waiter = self.cfn_client.get_waiter("stack_create_complete") try: waiter.wait( StackName=self.stack_name, WaiterConfig={"Delay": 10, "MaxAttempts": 60}, ) except WaiterError: logger.error("Stack creation failed or timed out.") self._print_stack_events() raise print("CloudFormation stack creation complete.") self._retrieve_stack_outputs() print(SEPARATOR) def _stack_exists(self, stack_name: str) -> bool: """Returns True if a stack with the given name exists and is not deleted.""" if not stack_name: return False try: response = self.cfn_client.describe_stacks(StackName=stack_name) stacks = response.get("Stacks", list()) for stack in stacks: status = stack.get("StackStatus", "") if "DELETE_COMPLETE" not in status: return True return False except ClientError as err: if "does not exist" in str(err): return False raise def _read_template(self) -> str: """Reads the CloudFormation template from the local file system.""" template_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), CFN_TEMPLATE_PATH ) if not os.path.exists(template_path): raise FileNotFoundError( f"CloudFormation template not found at {template_path}. " "Ensure the resources/cfn_template.yaml file exists." ) with open(template_path, "r") as f: return f.read() def _retrieve_stack_outputs(self) -> None: """Retrieves and displays outputs from the CloudFormation stack.""" response = self.cfn_client.describe_stacks(StackName=self.stack_name) stacks = response.get("Stacks", list()) if not stacks: raise RuntimeError(f"Stack '{self.stack_name}' not found.") outputs = stacks[0].get("Outputs", list()) for output in outputs: key = output.get("OutputKey", "") value = output.get("OutputValue", "") print(f"Stack output {key}: {value}") if key == "UserPoolId": self.user_pool_id = value elif key == "ClientId": self.client_id = value if not self.user_pool_id or not self.client_id: raise RuntimeError( "Failed to retrieve UserPoolId or ClientId from stack outputs." ) def _print_stack_events(self) -> None: """Prints recent stack events to help diagnose failures.""" try: response = self.cfn_client.describe_stack_events(StackName=self.stack_name) events = response.get("StackEvents", list()) for event in events[:10]: status = event.get("ResourceStatus", "") reason = event.get("ResourceStatusReason", "") resource = event.get("LogicalResourceId", "") if "FAILED" in status: print(f" FAILED: {resource} - {reason}") except ClientError: logger.warning("Could not retrieve stack events.") # ------------------------------------------------------------------ # Phase 2: Retrieve the app client secret # ------------------------------------------------------------------ def get_client_secret(self) -> None: """Calls DescribeUserPoolClient to retrieve the client secret.""" print(SEPARATOR) print("Retrieving the app client secret...") client_info = self.cognito_wrapper.describe_user_pool_client( self.user_pool_id, self.client_id ) self.client_secret = client_info.get("ClientSecret", "") if not self.client_secret: raise RuntimeError( "Client secret not found. Ensure the app client was created " "with GenerateSecret: true." ) print("Client secret retrieved successfully.") print(SEPARATOR) # ------------------------------------------------------------------ # Phase 3: Demonstrate GetClientToken # ------------------------------------------------------------------ def demonstrate_get_client_token(self) -> None: """Demonstrates obtaining M2M access tokens with GetClientToken.""" print(SEPARATOR) print("Obtaining an M2M access token with GetClientToken...") print() # 3a: Get token with all scopes (no scopes param = all configured scopes) print("Step 1: Request a token with all configured scopes.") auth_result = self.cognito_wrapper.get_client_token( client_id=self.client_id, client_secret=self.client_secret, ) self._display_token_result(auth_result, "all scopes") q.ask("\nPress Enter to continue...") # 3b: Get token with specific scopes print() print("Step 2: Request a token with a specific scope (my-m2m-api/read).") scoped_result = self.cognito_wrapper.get_client_token( client_id=self.client_id, client_secret=self.client_secret, scopes=["my-m2m-api/read"], ) self._display_token_result(scoped_result, "my-m2m-api/read") print() print("Token was successfully scoped to the requested permissions.") print(SEPARATOR) @staticmethod def _display_token_result(auth_result: dict, scope_description: str) -> None: """Displays token metadata.""" token_type = auth_result.get("TokenType", "Unknown") expires_in = auth_result.get("ExpiresIn", 0) access_token = auth_result.get("AccessToken", "") preview = access_token[:20] + "..." if len(access_token) > 20 else access_token print(f" Scope: {scope_description}") print(f" Token type: {token_type}") print(f" Expires in: {expires_in} seconds") print(f" Token preview: {preview}") # ------------------------------------------------------------------ # Phase 4: Demonstrate DescribeTermsByClient # ------------------------------------------------------------------ def demonstrate_describe_terms(self) -> None: """Demonstrates discovering Terms documents with DescribeTermsByClient.""" print(SEPARATOR) print("Discovering Terms documents with DescribeTermsByClient...") print() # 4a: Look up terms-of-use print("Looking up terms-of-use for the app client...") terms_of_use = self.cognito_wrapper.describe_terms_by_client( user_pool_id=self.user_pool_id, client_id=self.client_id, terms_name="terms-of-use", ) self._display_terms(terms_of_use) q.ask("\nPress Enter to continue...") print() # 4b: Look up privacy-policy print("Looking up privacy-policy for the app client...") privacy_policy = self.cognito_wrapper.describe_terms_by_client( user_pool_id=self.user_pool_id, client_id=self.client_id, terms_name="privacy-policy", ) self._display_terms(privacy_policy) print() print( "DescribeTermsByClient lets you discover which Terms documents are " "associated with a client without knowing the Terms resource ID up front." ) print(SEPARATOR) @staticmethod def _display_terms(terms: dict) -> None: """Displays a Terms document's details.""" print(f" Terms ID: {terms.get('TermsId', 'N/A')}") print(f" Terms name: {terms.get('TermsName', 'N/A')}") print(f" Client ID: {terms.get('ClientId', 'N/A')}") print(f" User pool ID: {terms.get('UserPoolId', 'N/A')}") print(f" Enforcement: {terms.get('Enforcement', 'N/A')}") print(f" Terms source: {terms.get('TermsSource', 'N/A')}") links = terms.get("Links", dict()) if links: print(" Links:") for lang_tag, url in links.items(): print(f" {lang_tag}: {url}") # Cognito returns CreationDate / LastModifiedDate as datetime objects # (boto3 deserializes the timestamps), so display them directly. creation_date = terms.get("CreationDate", None) if creation_date is not None: print(f" Created: {creation_date}") modified_date = terms.get("LastModifiedDate", None) if modified_date is not None: print(f" Last modified: {modified_date}") # ------------------------------------------------------------------ # Phase 5: Cleanup - delete CloudFormation stack # ------------------------------------------------------------------ def cleanup(self) -> None: """Prompts the user and deletes the CloudFormation stack.""" if self.stack_name is None: return print(SEPARATOR) answer = q.ask( "Do you want to delete the CloudFormation stack and all resources? (y/n) " ) if answer.lower() != "y": print( f"Resources will remain. You can delete the stack later " f"through the AWS Console or CLI." ) print(f"Stack name: {self.stack_name}") return print( f"CloudFormation stack '{self.stack_name}' is being deleted. " f"This may take a few minutes." ) try: self.cfn_client.delete_stack(StackName=self.stack_name) waiter = self.cfn_client.get_waiter("stack_delete_complete") print("Waiting for CloudFormation stack deletion to complete...") waiter.wait( StackName=self.stack_name, WaiterConfig={"Delay": 10, "MaxAttempts": 60}, ) print(f"CloudFormation stack '{self.stack_name}' has been deleted.") print("All Cognito resources have been removed.") except (ClientError, WaiterError): logger.warning( "Stack deletion encountered an error. Attempting force delete." ) try: self.cfn_client.delete_stack( StackName=self.stack_name, DeletionMode="FORCE_DELETE_STACK", ) print("Force delete initiated. Please verify in the console.") except ClientError: logger.error( "Force delete also failed. Clean up manually via the console." )Create a class that wraps Amazon Cognito Identity Provider operations.
class CognitoIdentityProviderWrapper: """Encapsulates Amazon Cognito Identity Provider actions.""" def __init__(self, cognito_idp_client: BaseClient): """ Initializes the CognitoIdentityProviderWrapper with an Amazon Cognito Identity Provider client. :param cognito_idp_client: A Boto3 Amazon Cognito Identity Provider client. """ self.cognito_idp_client = cognito_idp_client-
For API details, see the following topics in AWS SDK for Python (Boto3) API Reference.
-
For a complete list of AWS SDK developer guides and code examples, see Using this service with an AWS SDK. This topic also includes information about getting started and details about previous SDK versions.