Amazon Cognito User Authentication in Spring Boot REST

In the previous tutorial, we learned that how we can do User Authentication with Amazon Cognito in Spring Boot Application. In this tutorial, we will take our previous learnings and continue with the following.

  • Create Rest Controller to handle /login HTTP POST requests.
  • Read username and password from the request body to authenticate with Amazon Cognito User Pool.
  • Get a JWT access token and a refresh token in the HTTP response.

Amazon Cognito Configurations

There are certain settings that will be different in Amazon Cognito when we will authenticate users programmatically. We will have a look at them one by one.  First, we do not need to have any Domain which we used previously for the sign-up and sign-in pages that Amazon Cognito hosted. We will keep this empty.

Next, we can uncheck Generate client secret.

Then in  Auth Flows Configuration while creating App client we will check Enable username password auth for Admin APIs for authentication.

The rest of the settings may remain the same. Now we will head to our Spring Boot Application.

 

Integrating Spring Boot Application with Amazon Cognito

application.properties

Now let’s have a look at our application.properties file. In order to get aws-access-key and aws-access-secret we will first go to the Services tab and click on  IAM. It will redirect us to IAM Dashboard. Now we will click on Manage Access Keys.

Then we will click on Get New Access Key to download the file which contains aws-access-key and aws-access-secret. We will update our application.properties file accordingly.

We will update the value of aws.cognito.clientId to App client id in App Clients under General Settings. Similarly, we will update the value of aws.cognito.userPoolID and region to the Pool Id and region in General Settings.

aws.access-key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
aws.access-secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
aws.cognito.clientId= xxxxxxxxxxxxxxxxxxxxxxxxx
aws.cognito.userPoolId= xxxxxxxxxxxxxxxx
aws.cognito.region= xxxxxxx
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://cognito-idp.{region}.amazonaws.com/{Pool Id}

Rest Controller

In our Controller class, we have a Post API through which we will authenticate our user by passing username and password as payload which will then Amazon Cognito validate in our Service about which we will talk later in the Service section.

package com.example.cognitointegrationinspringrestapplication.rest;

import com.example.cognitointegrationinspringrestapplication.model.UserLoginRequestPayload;
import com.example.cognitointegrationinspringrestapplication.model.UserLoginResponsePayload;
import com.example.cognitointegrationinspringrestapplication.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(path = "/rest/users")
public class UserController {

    @Autowired
    UsersService usersService;

    @PostMapping(path = "/login")
    public ResponseEntity<Object> login(@RequestBody UserLoginRequestPayload userLoginRequestPayload) throws Exception {

        try {
            UserLoginResponsePayload userLoginResponsePayload = usersService.processLogin(userLoginRequestPayload);
            return new ResponseEntity<>(userLoginResponsePayload, HttpStatus.OK);
        } catch (Exception exception) {
            return new ResponseEntity<>(exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }

    }


}

Service

In our Service first, we will create an object of  AWSCognitoIdentityProvider and will pass it BasicAWSCredentials along with the region we have created our User Pool in. Then we will initiate the authorization request, as an administrator by creating an object of  AdminInitiateAuthRequest with AuthFlowType equal to ADMIN_USER_PASSWORD_AUTH  and pass Client Id, User Pool Id, username, and password passed by the user in the payload.

Note that initially, the Account status of the user is FORCE_CHANGE_PASSWORD. It will be changed to CONFIRMED only when the user provides the password set by the administrator along with the new password.  Now we will check whether the user has completed the challenge named NEW_PASSWORD_REQUIRED previously or not. If not then the user has to have a new password in the payload and only after that the user will be authenticated. On successful authentication, access_token and refresh_token will be returned in the response.

package com.example.cognitointegrationinspringrestapplication.service;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider;
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClientBuilder;
import com.amazonaws.services.cognitoidp.model.*;
import com.example.cognitointegrationinspringrestapplication.model.UserLoginRequestPayload;
import com.example.cognitointegrationinspringrestapplication.model.UserLoginResponsePayload;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;

@Service
public class UsersService {

    @Value(value = "${aws.cognito.userPoolId}")
    private String userPoolId;
    @Value(value = "${aws.cognito.clientId}")
    private String clientId;
    @Value(value = "${aws.cognito.region}")
    private String region;

    @Value(value = "${aws.access-key}")
    private String accessKey;
    @Value(value = "${aws.access-secret}")
    private String secretKey;
    

    public UserLoginResponsePayload processLogin(UserLoginRequestPayload userLoginRequestPayload) throws Exception {

        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);

        AWSCognitoIdentityProvider cognitoClient = AWSCognitoIdentityProviderClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds)).withRegion(region).build();

        UserLoginResponsePayload userLoginResponsePayload = new UserLoginResponsePayload();

        final Map<String, String> authParams = new HashMap<>();
        authParams.put("USERNAME", userLoginRequestPayload.getUserName());
        authParams.put("PASSWORD", userLoginRequestPayload.getPassword());

        final AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest();
        authRequest.withAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH).withClientId(clientId)
                .withUserPoolId(userPoolId).withAuthParameters(authParams);

        try {

            AdminInitiateAuthResult result = cognitoClient.adminInitiateAuth(authRequest);

            AuthenticationResultType authenticationResult = null;

            if (result.getChallengeName() != null && !result.getChallengeName().isEmpty()) {

                System.out.println("Challenge Name is " + result.getChallengeName());

                if (result.getChallengeName().contentEquals("NEW_PASSWORD_REQUIRED")) {
                    if (userLoginRequestPayload.getPassword() == null) {
                        throw new Exception("User must change password " + result.getChallengeName());

                    } else {

                        final Map<String, String> challengeResponses = new HashMap<>();
                        challengeResponses.put("USERNAME", userLoginRequestPayload.getUserName());
                        challengeResponses.put("PASSWORD", userLoginRequestPayload.getPassword());
                        // add new password
                        challengeResponses.put("NEW_PASSWORD", userLoginRequestPayload.getNewPassword());

                        final AdminRespondToAuthChallengeRequest request = new AdminRespondToAuthChallengeRequest()
                                .withChallengeName(ChallengeNameType.NEW_PASSWORD_REQUIRED)
                                .withChallengeResponses(challengeResponses).withClientId(clientId)
                                .withUserPoolId(userPoolId).withSession(result.getSession());

                        AdminRespondToAuthChallengeResult resultChallenge = cognitoClient
                                .adminRespondToAuthChallenge(request);
                        authenticationResult = resultChallenge.getAuthenticationResult();

                        userLoginResponsePayload.setAccessToken(authenticationResult.getAccessToken());
                        userLoginResponsePayload.setRefreshToken(authenticationResult.getRefreshToken());
                    }

                } else {
                    throw new Exception("User has other challenge " + result.getChallengeName());
                }

                cognitoClient.shutdown();
                return userLoginResponsePayload;
            } else {

                System.out.println("User has no challenge");
                authenticationResult = result.getAuthenticationResult();

                userLoginResponsePayload.setAccessToken(authenticationResult.getAccessToken());
                userLoginResponsePayload.setRefreshToken(authenticationResult.getRefreshToken());
                cognitoClient.shutdown();

                return userLoginResponsePayload;
            }

        } catch (InvalidParameterException e) {
            cognitoClient.shutdown();
            throw new Exception(e.getErrorMessage());
        } catch (Exception e) {
            cognitoClient.shutdown();
            throw new Exception(e.getMessage());
        }

    }
}

 

Payload

Following are the classes for passing data in Request and Response of APIs.

package com.example.cognitointegrationinspringrestapplication.model;

public class UserLoginRequestPayload {
    private String userName;
    private String password;
    private String newPassword;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNewPassword() {
        return newPassword;
    }

    public void setNewPassword(String newPassword) {
        this.newPassword = newPassword;
    }
}
package com.example.cognitointegrationinspringrestapplication.model;

public class UserLoginResponsePayload {

    private String accessToken;
    private String refreshToken;


    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }


    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }


}

pom.xml

Following is our pom.xml that contains all the necessary dependencies.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.0</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>cognito-integration-in-spring-rest-application</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cognito-integration-in-spring-rest-application</name>
    <description>Cognito Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-cognitoidp</artifactId>
            <version>1.11.1019</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Testing

When the user logs in for the first time, the new password has to be passed along with the password set by the administrator.

If we will not set a new password, we will get the following response.

 

Conclusion

With this, we have come to the end of our tutorial in which we did Amazon Cognito User Authentication in Spring Boot REST. First, we discussed the configuration changes needed in Amazon Cognito User Pool done in our previous tutorial so that we can continue with that User Pool in the current tutorial. Then we walked through the process of creating a Spring Boot Application and in the end we tested the POST API using Postman Client that returned access token and refresh token in the HTTP response.

Stay tuned for some more informative tutorials coming ahead and don’t forget to give your valuable feedback in the comments section.

Happy Learning!

Leave a Reply

Your email address will not be published. Required fields are marked *