Task 0 · 8 tasks

Identity with Cognito

Two kinds of identity: a machine-to-machine token for your services, and signed-in users for people.

15 minEasy
Alice’s ask

The “Enterprise Readiness” challenge

The local prototype won Alice over. Now she wants it in production for all 1,200 employees, and IT has one rule: nothing gets called without a verifiable identity.

In the original workshop this meant an hour of console clicking. Here the platform does it, and you spend the time understanding what a token actually carries.

Platform

What the platform provisions for you

terminal
uv run bootcamp.py up 0
  • A Cognito user pool for you, with a resource server and the scope datastream/mcp.access
  • An app client allowed to use the OAuth client credentials (M2M) flow
  • A Cognito domain that issues tokens (named ds-<10 hex chars>-<account>)
  • A second, user app client (username + password sign-in, no secret) and two DataStream users, alice-chen and jordan-lee, whose generated passwords are stored as SSM SecureString parameters under /workshop/awsworkshop-<name>-users/
  • Nothing for model access: your laptop already reaches the LiteLLM gateway keylessly with your participant role

Adding this stage usually takes ~6 min; the CLI prints progress (and full terraform output with --verbose).

Most of that time is a deliberate ~5 minute pause on the very first up 0: a new Cognito domain needs time for DNS to propagate. A heartbeat line every minute shows it is still working.

You

What you do as a developer

  1. Get a token

    terminal
    uv run bootcamp.py token

    That long string is a JWT. Every later stage (MCP runtime, Gateway, Agent) checks it.

  2. Read the token

    Challenge

    Decode your token and find the claim that lets you call MCP tools, and the claim that says which app client you are.

    Hint 1

    A JWT is three base64url parts separated by dots: header, payload, signature. The claims live in the payload; you don't need the signature to read them.

    Hint 2

    Split on ., take part 1, pad it to a multiple of 4 and base64.urlsafe_b64decode it. Look at scope and client_id.

    Solution
    terminal · macOS / Linux
    TOKEN=$(uv run bootcamp.py token)
    uv run python -c "import base64,json,sys; p=sys.argv[1].split('.')[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(p + '=' * (-len(p) % 4))), indent=2))" "$TOKEN"
    terminal · Windows PowerShell
    $TOKEN = uv run bootcamp.py token
    uv run python -c "import base64,json,sys; p=sys.argv[1].split('.')[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(p + '=' * (-len(p) % 4))), indent=2))" $TOKEN

    scope is datastream/mcp.access: the resource-server scope your runtimes and Gateway require. client_id identifies your M2M app client (the MCP runtime's and Gateway's JWT authorizers allow only that client). exp is when it expires.

  3. Sign in as a user

    Machines use the M2M token above. People sign in: the CLI reads Alice's password from SSM Parameter Store and calls Cognito InitiateAuth, exactly what a login page would do.

    bootcamp_cli/users.py
    def stored_password(session: boto3.Session, out: dict, username: str) -> str:
        """A story user's password from SSM Parameter Store (decrypted)."""
        name = f"{out['users_parameter_path']}/{username}"
        return session.client("ssm").get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"]
    
    
    def sign_in(session: boto3.Session, out: dict, username: str, password: str) -> str:
        """Sign in through the pool's user app client (no client secret) and return the user's access token."""
        response = session.client("cognito-idp").initiate_auth(
            ClientId=out["user_client_id"],
            AuthFlow="USER_PASSWORD_AUTH",
            AuthParameters={"USERNAME": username, "PASSWORD": password},
        )
        return response["AuthenticationResult"]["AccessToken"]
    terminal
    uv run bootcamp.py token --actor alice-chen

    Decode it like the M2M token: this one has a username claim (alice-chen), the user client's client_id, and no datastream/mcp.access scope. In Task 4 your agent accepts only this kind of token, and reads who is talking from username.

  4. See your ids

    terminal
    uv run bootcamp.py status

    Prints POOL_ID and friends. Later stages add more lines here.

Check your work

terminal
uv run bootcamp.py test --only 0

Four checks pass: Cognito issues a token for your client, both story users sign in with their stored passwords, litellm keyless identity (the gateway recognises your role) and no direct bedrock (a direct Bedrock call is refused with AccessDenied, as intended).

Under the hood

Amazon Cognito is AWS's identity provider. For service-to-service calls it uses the OAuth 2.0 client-credentials grant: your app client trades its id and secret for a short-lived access token scoped to datastream/mcp.access. AgentCore resources are configured with a JWT authorizer pointing at the pool's OIDC discovery URL, so they validate signatures and scopes without ever calling Cognito per request.

For people, the pool holds users. Signing in (InitiateAuth with USER_PASSWORD_AUTH) returns an access token whose username claim names the user, signed by the pool. A service that trusts that claim, and nothing the caller can simply type, knows who it is serving.

You work as the role bootcamp-participant-<name>, which the CLI assumes for you (BOOTCAMP_ROLE_ARN only overrides it). That role can only touch resources tagged Participant=<name> or named awsworkshop-<name>-*, and every role you create carries a permissions boundary that blocks direct Bedrock calls: LiteLLM is the only path to a model.