AWS Cognito User Pools

2023/08/11 3:28PM

Description

In this blog post I will show how you can create your own Sign-in/Sign-up pages in React and integrate it with AWS Cognito. If you don't know how to setup AWS cognito, check out my previous blog post.

Step 1: Boilerplate

You can download the code needed for the UI from my GitHub:

https://github.com/JoshMorrison99/learn-aws-cognito
Step 2: Integrating Cognito Functionality

The first thing we need to do is download the cognito library.

npm install amazon-cognito-identity-js

Next we will need to setup the `UserPool.js` file which will contain the `ClientId` and `UserPoolId` for our AWS Cognito application. Note: Change the `UserPoolId` and `ClientId` to be for your account if you are following along.


    import { CognitoUserPool } from "amazon-cognito-identity-js";

    const poolData = {
        UserPoolId: 'us-east-1_gBPTxlyKI',
        ClientId: '5015ncg0dvmsd4vbmskke3j1tv'
    }
    
    export default new CognitoUserPool(poolData)

The `UserPoolId` can be found here:

- The User Pool ID is a unique identifier for your Amazon Cognito User Pool. It's used to identify the user pool that your application is associated with. While it's not a sensitive secret, it's recommended to keep it private and not expose it directly to end-users.

The `ClientId` can be found here:

- The Client ID is associated with an app that you register within your User Pool. It's used by your application to authenticate and communicate with the User Pool. While it's not a password or secret, it's still important to keep it private because it allows applications to interact with the User Pool on behalf of users.

Step 3: Signup Functionality

In the previous blog we configured the user pool sign-in options like so. We selected both User name and Email for the sign-in options meaning that when a user signs up, they will need to supply both a username and an email in signup process.

The following error will be received if the signup process only involves sending an email with no username:

InvalidParameterException: Username cannot be of email format, since user pool is configured for email alias.

*Sending only email and password*


    const onSubmit = (event) => {
        event.preventDefault();
        
        UserPool.signUp(email, password, [], null, (err, data) => {
            if(err) {
                console.log(err)
            }else{
                console.log(data)
            }
        })

If we send only a username, then we get the following error:

InvalidParameterException: Attributes did not conform to the schema: email: The attribute is required

*Sending only username and password*


    const onSubmit = (event) => {
        event.preventDefault();
        
        UserPool.signUp(username, password, [], null, (err, data) => {
            if(err) {
                console.log(err)
            }else{
                console.log(data)
            }
        })

To add the email attribute to our Signup request, we need to add it in the attributeList.


    var AmazonCognitoIdentity = require('amazon-cognito-identity-js');

    const [email, setEmail] = useState("")
    const [username, setUsername] = useState("")
    const [password, setPassword] = useState("")
    const [confirmPassword, setConfirmPassword] = useState("")
    
    const onSubmit = (event) => {
        event.preventDefault();
    
        var attributeList = [];
    
        var dataEmail = {
            Name: 'email',
            Value: email
        }
        
        var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);
        attributeList.push(attributeEmail);
    
        UserPool.signUp(username, password, attributeList, null, (err, data) => {
            if(err) {
                console.log(err)
            }else{
                console.log(data)
            }
        })
      }

As long as the password passes the password policy configured and the user doesn't already exists, then we will get the following in response noting that we successfully registered a new cognito user.


    {
        "user": {
            "username": "testuser",
            "pool": {
                "userPoolId": "us-east-1_gBPTxlyKI",
                "clientId": "5015ncg0dvmsd4vbmskke3j1tv",
                "client": {
                    "endpoint": "https://cognito-idp.us-east-1.amazonaws.com/",
                    "fetchOptions": {}
                },
                "advancedSecurityDataCollectionFlag": true,
                "storage": {
                    "ally-supports-cache": "{\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0;..."
                }
            },
            "Session": null,
            "client": {
                "endpoint": "https://cognito-idp.us-east-1.amazonaws.com/",
                "fetchOptions": {}
            },
            "signInUserSession": null,
            "authenticationFlowType": "USER_SRP_AUTH",
            "storage": {
                "ally-supports-cache": "{\"userAgent\":\"Mozilla/5.0 (Windows NT 10.0; ..."
            },
            "keyPrefix": "CognitoIdentityServiceProvider.5015ncg0dvmsd4vbmskke3j1tv",
            "userDataKey": "CognitoIdentityServiceProvider.5015ncg0dvmsd4vbmskke3j1tv.testuser.userData"
        },
        "userConfirmed": false,
        "userSub": "6c86045f-f73c-49c7-b91c-864aa7a74f9a",
        "codeDeliveryDetails": {
            "AttributeName": "email",
            "DeliveryMedium": "EMAIL",
            "Destination": "t***@g***"
        }
    }

If we go back to the AWS cognito dashboard, we can see that our new user was successfully added.

Step 4: Login Functionality

The login functionality is similar to the register functionality. For this process we will need to create a `CognitoUser` and supply `AuthDetails`. Since we configured the login to accept either username or email, I will only supply a user field for convenience.

If the user tries to login without supplying a username, the following error will occur:

InvalidParameterException: Missing required parameter USERNAME

If the user supplies a username that doesn't exist then the following error will occur:

NotAuthorizedException: Incorrect username or password.

If the user supplies a username that does exist, but incorrect password, then the following error will occur:

NotAuthorizedException: Incorrect username or password.

Therefore, by default, AWS `cognito` is immune to username enumeration.

If the user supplies the correct username and password, but hasn't confirmed their email, the the following error will occur:

UserNotConfirmedException: User is not confirmed.

From the AWS Cognito console, we can manually confirm the user and verify their email address.

In addition, I also disabled MFA.

If the user supplies the correct username and password, with verified email, confirmed account and MFA, then the following object is returned.


    {
        "idToken": {
            "jwtToken": "eyJraWQiOiJ6eUZzVyttRURYek5maHRZbHBTN2xTOE9HUVRoSjRmK2pjTnc0WnY...",
            "payload": {
                "sub": "6c86045f-f73c-49c7-b91c-864aa7a74f9a",
                "email_verified": true,
                "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_gBPTxlyKI",
                "cognito:username": "testuser",
                "origin_jti": "bb83a74c-ff2e-4fdd-bb30-345cd80ff0f4",
                "aud": "5015ncg0dvmsd4vbmskke3j1tv",
                "event_id": "e8860367-44d6-48b0-8ba9-92ea7a86193c",
                "token_use": "id",
                "auth_time": 1691766912,
                "exp": 1691770511,
                "iat": 1691766912,
                "jti": "5b09b1bf-8989-4c29-8514-fa9a03798729",
                "email": "testuser@gmail.com"
            }
        },
        "refreshToken": {
            "token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2..."
        },
        "accessToken": {
            "jwtToken": "eyJraWQiOiJueXdGS0pudTRiU1ZTdk5PTGxwNzVJS0hlSUpxRzNtWkp0NnpcL2FRQ...",
            "payload": {
                "sub": "6c86045f-f73c-49c7-b91c-864aa7a74f9a",
                "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_gBPTxlyKI",
                "client_id": "5015ncg0dvmsd4vbmskke3j1tv",
                "origin_jti": "bb83a74c-ff2e-4fdd-bb30-345cd80ff0f4",
                "event_id": "e8860367-44d6-48b0-8ba9-92ea7a86193c",
                "token_use": "access",
                "scope": "aws.cognito.signin.user.admin",
                "auth_time": 1691766912,
                "exp": 1691770511,
                "iat": 1691766912,
                "jti": "3513b90a-3b45-4248-a732-fb5161826d80",
                "username": "testuser"
            }
        },
        "clockDrift": -1
    }

Authenticate users and grant access to resources with tokens. The claims in tokens are information about your user. The ID token contains claims about their identity, like their username, family name, and email address. The access token contains claims like scope that the authenticated user can use to access third-party APIs, Amazon Cognito user self-service API operations, and the UserInfo endpoint. The access and ID token both include a cognito:groups claim that contains your user's group membership in your user pool.

Amazon Cognito also has tokens that you can use to get new tokens or revoke existing tokens. Refresh a token to retrieve a new ID and access tokens. Revoke a token to revoke user access that is allowed by refresh tokens.

Amazon Cognito issues tokens as Base64-encoded strings. You can decode any Amazon Cognito ID or access token from base64 to plaintext JSON. Amazon Cognito refresh tokens are encrypted, and can't be read by Amazon Cognito administrators or users.

In the next blog post on AWS Cognito User Pools I will go into how to pentest the service.