# Cognito User Pools

{{#include ../../../../banners/hacktricks-training.md}}

## Basic Information

A user pool is a user directory in Amazon Cognito. With a user pool, your users can **sign in to your web or mobile app** through Amazon Cognito, **or federate** through a **third-party** identity provider (IdP). Whether your users sign in directly or through a third party, all members of the user pool have a directory profile that you can access through an SDK.

User pools provide:

- Sign-up and sign-in services.
- A built-in, customizable web UI to sign in users.
- Social sign-in with Facebook, Google, Login with Amazon, and Sign in with Apple, and through SAML and OIDC identity providers from your user pool.
- User directory management and user profiles.
- Security features such as multi-factor authentication (MFA), checks for compromised credentials, account takeover protection, and phone and email verification.
- Customized workflows and user migration through AWS Lambda triggers.

**Source code** of applications will usually also contain the **user pool ID** and the **client application ID**, (and some times the **application secret**?) which are needed for a **user to login** to a Cognito User Pool.

### Potential attacks

- **Registration**: By default a user can register himself, so he could create a user for himself.
- **User enumeration**: The registration functionality can be used to find usernames that already exists. This information can be useful for the brute-force attack.
- **Login brute-force**: In the [**Authentication**](cognito-user-pools.md#authentication) section you have all the **methods** that a user have to **login**, you could try to brute-force them **find valid credentials**.

### Tools for pentesting

- [Pacu](https://github.com/RhinoSecurityLabs/pacu), now includes the `cognito__enum` and `cognito__attack` modules that automate enumeration of all Cognito assets in an account and flag weak configurations, user attributes used for access control, etc., and also automate user creation (including MFA support) and privilege escalation based on modifiable custom attributes, usable identity pool credentials, assumable roles in id tokens, etc.\
  For a description of the modules' functions see part 2 of the [blog post](https://rhinosecuritylabs.com/aws/attacking-aws-cognito-with-pacu-p2). For installation instructions see the main [Pacu](https://github.com/RhinoSecurityLabs/pacu) page.

```bash
# Run cognito__enum usage to gather all user pools, user pool clients, identity pools, users, etc. visible in the current AWS account
Pacu (new:test) > run cognito__enum

# cognito__attack usage to attempt user creation and all privesc vectors against a given identity pool and user pool client:
Pacu (new:test) > run cognito__attack --username randomuser --email XX+sdfs2@gmail.com --identity_pools
us-east-2:a06XXXXX-c9XX-4aXX-9a33-9ceXXXXXXXXX --user_pool_clients
59f6tuhfXXXXXXXXXXXXXXXXXX@us-east-2_0aXXXXXXX
```

- [Cognito Scanner](https://github.com/padok-team/cognito-scanner) is a CLI tool in python that implements different attacks on Cognito including unwanted account creation and account oracle. Check [this link](https://github.com/padok-team/cognito-scanner) for more info.

```bash
# Install
pip install cognito-scanner
# Run
cognito-scanner --help
```

- [CognitoAttributeEnum](https://github.com/punishell/CognitoAttributeEnum): This script allows to enumerate valid attributes for users.

```bash
python cognito-attribute-enu.py -client_id 16f1g98bfuj9i0g3f8be36kkrl
```

## Registration

User Pools allows by **default** to **register new users**.

```bash
aws cognito-idp sign-up --client-id <client-id> \
    --username <username> --password <password> \
    --region <region> --no-sign-request
```

#### If anyone can register

You might find an error indicating you that you need to **provide more details** of abut the user:

```
An error occurred (InvalidParameterException) when calling the SignUp operation: Attributes did not conform to the schema: address: The attribute is required
```

You can provide the needed details with a JSON such as:

```json
--user-attributes '[{"Name": "email", "Value": "carlospolop@gmail.com"}, {"Name":"gender", "Value": "M"}, {"Name": "address", "Value": "street"}, {"Name": "custom:custom_name", "Value":"supername&\"*$"}]'
```

You could use this functionality also to **enumerate existing users.** This is the error message when a user already exists with that name:

```
An error occurred (UsernameExistsException) when calling the SignUp operation: User already exists
```

> [!NOTE]
> Note in the previous command how the **custom attributes start with "custom:"**.\
> Also know that when registering you **cannot create for the user new custom attributes**. You can only give value to **default attributes** (even if they aren't required) and **custom attributes specified**.

Or just to test if a client id exists. This is the error if the client-id doesn't exist:

```
An error occurred (ResourceNotFoundException) when calling the SignUp operation: User pool client 3ig612gjm56p1ljls1prq2miut does not exist.
```

#### If only admin can register users

You will find this error and you own't be able to register or enumerate users:

```
An error occurred (NotAuthorizedException) when calling the SignUp operation: SignUp is not permitted for this user pool
```

### Verifying Registration

Cognito allows to **verify a new user by verifying his email or phone number**. Therefore, when creating a user usually you will be required at least the username and password and the **email and/or telephone number**. Just set one **you control** so you will receive the code to **verify your** newly created user **account** like this:

```bash
aws cognito-idp confirm-sign-up --client-id <cliet_id> \
    --username aasdasd2 --confirmation-code <conf_code> \
    --no-sign-request --region us-east-1
```

> [!WARNING]
> Even if **looks like you can use the same email** and phone number, when you need to verify the created user Cognito will complain about using the same info and **won't let you verify the account**.

### Privilege Escalation / Updating Attributes

By default a user can **modify the value of his attributes** with something like:

```bash
aws cognito-idp update-user-attributes \
    --region us-east-1 --no-sign-request \
    --user-attributes Name=address,Value=street \
    --access-token <access token>
```

#### Custom attribute privesc

> [!CAUTION]
> You might find **custom attributes** being used (such as `isAdmin`), as by default you can **change the values of your own attributes** you might be able to **escalate privileges** changing the value yourself!

#### Email/username modification privesc

You can use this to **modify the email and phone number** of a user, but then, even if the account remains as verified, those attributes are **set in unverified status** (you need to verify them again).

> [!WARNING]
> You **won't be able to login with email or phone number** until you verify them, but you will be **able to login with the username**.\
> Note that even if the email was modified and not verified it will appear in the ID Token inside the **`email`** **field** and the filed **`email_verified`** will be **false**, but if the app **isn't checking that you might impersonate other users**.

> Moreover, note that you can put anything inside the **`name`** field just modifying the **name attribute**. If an app is **checking** **that** field for some reason **instead of the `email`** (or any other attribute) you might be able to **impersonate other users**.

Anyway, if for some reason you changed your email for example to a new one you can access you can **confirm the email with the code you received in that email address**:

```bash
aws cognito-idp verify-user-attribute \
    --access-token <access_token> \
    --attribute-name email --code <code> \
    --region <region> --no-sign-request
```

Use **`phone_number`** instead of **`email`** to change/verify a **new phone number**.

> [!NOTE]
> The admin could also enable the option to **login with a user preferred username**. Note that you won't be able to change this value to **any username or preferred_username already being used** to impersonate a different user.

### Recover/Change Password

It's possible to recover a password just **knowing the username** (or email or phone is accepted) and having access to it as a code will be sent there:

```bash
aws cognito-idp forgot-password \
    --client-id <client_id> \
    --username <username/email/phone> --region <region>
```

> [!NOTE]
> The response of the server is always going to be positive, like if the username existed. You cannot use this method to enumerate users

With the code you can change the password with:

```bash
aws cognito-idp confirm-forgot-password \
    --client-id <client_id> \
    --username <username> \
    --confirmation-code <conf_code> \
    --password <pwd> --region <region>
```

To change the password you need to **know the previous password**:

```bash
aws cognito-idp change-password \
    --previous-password <value> \
    --proposed-password <value> \
    --access-token <value>
```

## Authentication

A user pool supports **different ways to authenticate** to it. If you have a **username and password** there are also **different methods** supported to login.\
Moreover, when a user is authenticated in the Pool **3 types of tokens are given**: The **ID Token**, the **Access token** and the **Refresh token**.

- [**ID Token**](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html): It contains claims about the **identity of the authenticated user,** such as `name`, `email`, and `phone_number`. The ID token can also be used to **authenticate users to your resource servers or server applications**. You must **verify** the **signature** of the ID token before you can trust any claims inside the ID token if you use it in external applications.
  - The ID Token is the token that **contains the attributes values of the user**, even the custom ones.
- [**Access Token**](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-access-token.html): It contains claims about the authenticated user, a list of the **user's groups, and a list of scopes**. The purpose of the access token is to **authorize API operations** in the context of the user in the user pool. For example, you can use the access token to **grant your user access** to add, change, or delete user attributes.
- [**Refresh Token**](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-refresh-token.html): With refresh tokens you can **get new ID Tokens and Access Tokens** for the user until the **refresh token is invalid**. By **default**, the refresh token **expires 30 days after** your application user signs into your user pool. When you create an application for your user pool, you can set the application's refresh token expiration to **any value between 60 minutes and 10 years**.

### ADMIN_NO_SRP_AUTH & ADMIN_USER_PASSWORD_AUTH

This is the server side authentication flow:

- The server-side app calls the **`AdminInitiateAuth` API operation** (instead of `InitiateAuth`). This operation requires AWS credentials with permissions that include **`cognito-idp:AdminInitiateAuth`** and **`cognito-idp:AdminRespondToAuthChallenge`**. The operation returns the required authentication parameters.
- After the server-side app has the **authentication parameters**, it calls the **`AdminRespondToAuthChallenge` API operation**. The `AdminRespondToAuthChallenge` API operation only succeeds when you provide AWS credentials.

This **method is NOT enabled** by default.

To **login** you **need** to know:

- user pool id
- client id
- username
- password
- client secret (only if the app is configured to use a secret)

> [!NOTE]
> In order to be **able to login with this method** that application must allow to login with `ALLOW_ADMIN_USER_PASSWORD_AUTH`.\
> Moreover, to perform this action you need credentials with the permissions **`cognito-idp:AdminInitiateAuth`** and **`cognito-idp:AdminRespondToAuthChallenge`**

```python
aws cognito-idp admin-initiate-auth \
    --client-id <client-id> \
    --auth-flow ADMIN_USER_PASSWORD_AUTH \
    --region <region> \
    --auth-parameters 'USERNAME=<username>,PASSWORD=<password>,SECRET_HASH=<hash_if_needed>'
    --user-pool-id "<pool-id>"

# Check the python code to learn how to generate the hsecret_hash
```

<details>

<summary>Code to Login</summary>

```python
import boto3
import botocore
import hmac
import hashlib
import base64


client_id = "<client-id>"
user_pool_id = "<user-pool-id>"
client_secret = "<client-secret>"
username = "<username>"
password = "<pwd>"

boto_client = boto3.client('cognito-idp', region_name='us-east-1')

def get_secret_hash(username, client_id, client_secret):
    key = bytes(client_secret, 'utf-8')
    message = bytes(f'{username}{client_id}', 'utf-8')
    return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()

# If the Client App isn't configured to use a secret
## just delete the line setting the SECRET_HASH
def login_user(username_or_alias, password, client_id, client_secret, user_pool_id):
    try:
        return boto_client.admin_initiate_auth(
            UserPoolId=user_pool_id,
            ClientId=client_id,
            AuthFlow='ADMIN_USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username_or_alias,
                'PASSWORD': password,
                'SECRET_HASH': get_secret_hash(username_or_alias, client_id, client_secret)
            }
        )
    except botocore.exceptions.ClientError as e:
        return e.response

print(login_user(username, password, client_id, client_secret, user_pool_id))
```

</details>

### USER_PASSWORD_AUTH

This method is another simple and **traditional user & password authentication** flow. It's recommended to **migrate a traditional** authentication method **to Cognito** and **recommended** to then **disable** it and **use** then **ALLOW_USER_SRP_AUTH** method instead (as that one never sends the password over the network).\
This **method is NOT enabled** by default.

The main **difference** with the **previous auth method** inside the code is that you **don't need to know the user pool ID** and that you **don't need extra permissions** in the Cognito User Pool.

To **login** you **need** to know:

- client id
- username
- password
- client secret (only if the app is configured to use a secret)

> [!NOTE]
> In order to be **able to login with this method** that application must allow to login with ALLOW_USER_PASSWORD_AUTH.

```python
aws cognito-idp initiate-auth  --client-id <client-id> \
    --auth-flow USER_PASSWORD_AUTH --region <region> \
    --auth-parameters 'USERNAME=<username>,PASSWORD=<password>,SECRET_HASH=<hash_if_needed>'

# Check the python code to learn how to generate the secret_hash
```

<details>

<summary>Python code to Login</summary>

```python
import boto3
import botocore
import hmac
import hashlib
import base64


client_id = "<client-id>"
user_pool_id = "<user-pool-id>"
client_secret = "<client-secret>"
username = "<username>"
password = "<pwd>"

boto_client = boto3.client('cognito-idp', region_name='us-east-1')

def get_secret_hash(username, client_id, client_secret):
    key = bytes(client_secret, 'utf-8')
    message = bytes(f'{username}{client_id}', 'utf-8')
    return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()

# If the Client App isn't configured to use a secret
## just delete the line setting the SECRET_HASH
def login_user(username_or_alias, password, client_id, client_secret, user_pool_id):
    try:
        return boto_client.initiate_auth(
            ClientId=client_id,
            AuthFlow='ADMIN_USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username_or_alias,
                'PASSWORD': password,
                'SECRET_HASH': get_secret_hash(username_or_alias, client_id, client_secret)
            }
        )
    except botocore.exceptions.ClientError as e:
        return e.response

print(login_user(username, password, client_id, client_secret, user_pool_id))
```

</details>

### USER_SRP_AUTH

This is scenario is similar to the previous one but **instead of of sending the password** through the network to login a **challenge authentication is performed** (so no password navigating even encrypted through he net).\
This **method is enabled** by default.

To **login** you **need** to know:

- user pool id
- client id
- username
- password
- client secret (only if the app is configured to use a secret)

<details>

<summary>Code to login</summary>

```python
from warrant.aws_srp import AWSSRP
import os

USERNAME='xxx'
PASSWORD='yyy'
POOL_ID='us-east-1_zzzzz'
CLIENT_ID = '12xxxxxxxxxxxxxxxxxxxxxxx'
CLIENT_SECRET = 'secreeeeet'
os.environ["AWS_DEFAULT_REGION"] = "<region>"

aws = AWSSRP(username=USERNAME, password=PASSWORD, pool_id=POOL_ID,
    client_id=CLIENT_ID, client_secret=CLIENT_SECRET)
tokens = aws.authenticate_user()
id_token = tokens['AuthenticationResult']['IdToken']
refresh_token = tokens['AuthenticationResult']['RefreshToken']
access_token = tokens['AuthenticationResult']['AccessToken']
token_type = tokens['AuthenticationResult']['TokenType']
```

</details>

### REFRESH_TOKEN_AUTH & REFRESH_TOKEN

This **method is always going to be valid** (it cannot be disabled) but you need to have a valid refresh token.

```bash
aws cognito-idp initiate-auth \
    --client-id 3ig6h5gjm56p1ljls1prq2miut \
    --auth-flow REFRESH_TOKEN_AUTH \
    --region us-east-1 \
    --auth-parameters 'REFRESH_TOKEN=<token>'
```

<details>

<summary>Code to refresh</summary>

```python
import boto3
import botocore
import hmac
import hashlib
import base64

client_id = "<client-id>"
token = '<token>'

boto_client = boto3.client('cognito-idp', region_name='<region>')

def refresh(client_id, refresh_token):
    try:
        return boto_client.initiate_auth(
            ClientId=client_id,
            AuthFlow='REFRESH_TOKEN_AUTH',
            AuthParameters={
                'REFRESH_TOKEN': refresh_token
            }
        )
    except botocore.exceptions.ClientError as e:
        return e.response


print(refresh(client_id, token))
```

</details>

### CUSTOM_AUTH

In this case the **authentication** is going to be performed through the **execution of a lambda function**.

## Extra Security

### Advanced Security

By default it's disabled, but if enabled, Cognito could be able to **find account takeovers**. To minimise the probability you should login from a **network inside the same city, using the same user agent** (and IP is thats possible)**.**

### **MFA Remember device**

If the user logins from the same device, the MFA might be bypassed, therefore try to login from the same browser with the same metadata (IP?) to try to bypass the MFA protection.

## User Pool Groups IAM Roles

It's possible to add **users to User Pool** groups that are related to one **IAM roles**.\
Moreover, **users** can be assigned to **more than 1 group with different IAM roles** attached.

Note that even if a group is inside a group with an IAM role attached, in order to be able to access IAM credentials of that group it's needed that the **User Pool is trusted by an Identity Pool** (and know the details of that Identity Pool).

Another requisite to get the **IAM role indicated in the IdToken** when a user is authenticated in the User Pool (`aws cognito-idp initiate-auth...`) is that the **Identity Provider Authentication provider** needs indicate that the **role must be selected from the token.**

<figure><img src="../../../../images/image (250).png" alt=""><figcaption></figcaption></figure>

The **roles** a user have access to are **inside the `IdToken`**, and a user can **select which role he would like credentials for** with the **`--custom-role-arn`** from `aws cognito-identity get-credentials-for-identity`.\
However, if the **default option** is the one **configured** (`use default role`), and you try to access a role from the IdToken, you will get **error** (that's why the previous configuration is needed):

```
An error occurred (InvalidParameterException) when calling the GetCredentialsForIdentity operation: Only SAML providers and providers with RoleMappings support custom role ARN.
```

> [!WARNING]
> Note that the role assigned to a **User Pool Group** needs to be **accesible by the Identity Provider** that **trust the User Pool** (as the IAM role **session credentials are going to be obtained from it**).

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "cognito-identity.amazonaws.com:aud": "us-east-1:2361092e-9db6-a876-1027-10387c9de439"
                },
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "authenticated"
                }
            }
        }
    ]
}js
```

{{#include ../../../../banners/hacktricks-training.md}}




