Blog

Java Integration with Amazon Cognito Developer Tutorial

In this developer tutorial, we are going to learn how to make an integration with Amazon Cognito using the Amazon Web Services software development kit (AWS SDK) for Java by providing some code samples and documentation. We are going to leverage AWS to integrate authentication and authorization into a Java web application, in addition to using groups in Amazon Cognito user and identity pools to obtain temporary identity and access management (IAM) credentials in the application.

Let’s start from the beginning.

 

What is Amazon Cognito?

Amazon Cognito is a simple user identity and data synchronization service that provides authentication, authorization, and user management, helping us securely manage app data across applications for our users. Amazon Cognito allows us to control permissions for different users’ groups in our applications to ensure that they have appropriate access to backend resources according to the group they belong to.

Main components

 

Amazon Cognito Pools

 

Steps to achieve authentication and authorization with Cognito

  1. Sign in to the Amazon Cognito console.
  2. Go to AWS and find Cognito under the ‘Security, Identity & Compliance’ section.

Cognito Menu Option

  1. On the ‘Your User Pools’ page, choose ‘Create a User Pool.’

Create User Pool Button

  1. Create an identity pool and configure it to integrate with the user pool.

Identity Pool Options

  1.    Create an IAM role and add a specific AWS access.
  2.    Create a group in the user pool and map it to the new IAM role.

Create Group

 

Integrating Cognito with Java

The main Cognito Java classes we will be using in our Java application are:

  1. AWSCognitoIdentityProvider: The AWSCognitoIdentityProvider class allows us to execute a lot of actions, some of the most useful being:
  •      Add Custom Attributes
  •      Admin Add User to Group
  •      Admin Confirm Sign Up
  •      Admin Create User
  •      Admin Delete User
  •      Admin Reset User Password
  •      Admin Initiate Auth
  •      Admin Enable User
  •      Change Password
  •      Create Group
  •      Delete User
  •      Delete Group
  •      Delete User
  •      Forgot Password
  •      Get User
  •      Global Sign Out
  •      List Groups
  •      List Users
  •      Sign Up
  •      Update Group
  •      Update User Attributes
  •      … More
     

You can see the entire API Reference here.

  1. ClasspathPropertiesFileCredentialsProvider: Of all the different AWS credentials providers, we are only going to be using the ClasspathPropertiesFileCredentialsProvider in this guide.

In order to use this class, we need to create an AwsCredentials.properties file in our classpath. The file should look like this:

AWS Properties

 

Let’s code

We are going to be creating a Maven web project in Java.

To start with the integration, we have to declare the AWS SDK dependencies in the pom.xml of our project.

Maven dependencies:

<dependency> <groupId>com.amazonaws aws-java-sdk 1.11.360 com.amazonaws aws-java-sdk-core 1.11.360 com.amazonaws aws-java-sdk-cognitoidp 1.11.360

The second step is to create our AWSCognitoIdentityProvider using the credentials we have in the AwsCredentials.properties file.

Important: When you see a call to a Cognito Config, it is a call to a property file that has the following information:

clientId = 2jkihs1a8su8n4jq0lvihsh3po
userPoolId = us-east-1_3vocxnITQ
endpoint = cognito-idp.us-east-1.amazonaws.com
region = us-east-1
identityPoolId = us-east-1:f2810be3-a906-4a1e-83bc-aa1230b6789

public AWSCognitoIdentityProvider getAmazonCognitoIdentityClient() {
      ClasspathPropertiesFileCredentialsProvider propertiesFileCredentialsProvider =
           new ClasspathPropertiesFileCredentialsProvider();

       return AWSCognitoIdentityProviderClientBuilder.standard()
                      .withCredentials(propertiesFileCredentialsProvider)
                             .withRegion(cognitoConfig.getRegion())
                             .build();

   }

Once we have defined the Cognito client, we can start calling the API services. Let’s look at how to sign up, sign in, add a user to a group, change a user’s password and make a ‘get user info’ API request.

 

Sign Up Implementation:

Sign Up Implementation

While implementing the ‘sign up’ functionality, you get a set of default attributes; these attributes in Cognito are called ‘standard attributes.’ In addition to these, Cognito also allows you to add custom attributes to your specific user pool definition in the AWS console.

These are the Cognito standard attributes: address, birthdate, email, family name, gender, given name, location, middle name, last name, nickname, phone number, picture, preferred username, profile, time zone, ‘updated at’ time, and website.

In this sign-up implementation example, we are going to be using two custom fields: the company position and the company name.

public static UserType signUp(UserSignUpRequest signUpRequest){
        AWSCognitoIdentityProvider cognitoClient = getAmazonCognitoIdentityClient();
        AdminCreateUserRequest cognitoRequest = new AdminCreateUserRequest()
                .withUserPoolId("us-east-1_Qqtfujski")
                .withUsername(signUpRequest.getUsername())
                .withUserAttributes(
                new AttributeType()
                        .withName("email")
                        .withValue(signUpRequest.getEmail()),
                new AttributeType()
                        .withName("name")
                        .withValue(signUpRequest.getName()),
                new AttributeType()
                        .withName("family_name")
                        .withValue(signUpRequest.getLastName()),
                new AttributeType()
                        .withName("phone_number")
                        .withValue(signUpRequest.getPhoneNumber()),
                new AttributeType()
                        .withName("custom:companyName")
                        .withValue(signUpRequest.getCompanyName()),
                new AttributeType()
                        .withName("custom:companyPosition")
                        .withValue(signUpRequest.getCompanyPosition()),
                new AttributeType()
                        .withName("email_verified")
                        .withValue("true"))
                .withTemporaryPassword("!j8fkxv2oTjLEMd")
                .withMessageAction("SUPPRESS")
                .withDesiredDeliveryMediums(DeliveryMediumType.EMAIL)
                .withForceAliasCreation(Boolean.FALSE);
        AdminCreateUserResult createUserResult =  cognitoClient.adminCreateUser(cognitoRequest);
        UserType cognitoUser =  createUserResult.getUser();

        return cognitoUser;
    }

 

With Message Action: If the message action is not set, the default is to send a welcome message via email or phone (SMS). The welcome message includes custom sign up instructions, the username, and a temporary password. When we execute the withMessageAction suppress option, Amazon Cognito will not send any email, and in this case, the user will be in the FORCE_CHANGE_PASSWORD state until they sign in and change their password.

With Temporary Password: This parameter is not required, and if you don’t specify a value, Amazon Cognito generates one for you. In the example shown, we defined a temporary password.

With Desired Delivery Mediums: This parameter is not required; if you don’t specify a value, the default value is “SMS.” In this case, we chose ‘email,’ but if we want, we can select both email and SMS.

With Force Alias Creation: This parameter is not required and is only used if the ‘phone number verified’ or the ‘email verified’ attribute is set to ‘true.’ Otherwise, it is ignored. If we set this to ‘true,’ and the specified phone number or email address already exists as an alias with a different user, the API call is going to migrate the alias from the existing user to the newly created user.

 

Sign In Implementation:

In this example, we are going to call two functions of the AWS Cognito Identity Provider. First, we are going make the ‘Admin Initiate Auth Request,’ and if the user is on the FORCE_CHANGE_PASSWORD status, we are going to call the ‘Admin Respond To Auth Challenge.’

The Admin Initiate Auth Request: This initiates the authentication flow as an administrator. If the action is successful, it returns an authentication response with an access token, ‘expires in’ time, ID token, refresh token and a token type. If the request needs another challenge before it gets the token’s challenge name, the challenge parameters and session are returned.

The Admin Respond To Auth Challenge Request: This responds to an authentication challenge as an administrator. It requires the challenge name, the client ID, the user pool ID, the session, and the challenge responses. If the action is successful, it returns an authentication response with an access token, ‘expires in’ time, ID token, refresh token and a token type.

They are a lot of challenge types, such as:

  • MFA_SETUP: If Multi-Factor Authentication (MFA) is required, users who do not have at least one of the MFA methods set up are presented with an MFA_SETUP challenge. The user must set up at least one MFA type to continue authenticating.
  • SELECT_MFA_TYPE: This selects the MFA type. Valid MFA options are SMS_MFA for MFA via SMS, and SOFTWARE_TOKEN_MFA for TOTP software token MFA.
  • SMS_MFA: The next challenge is to supply an SMS_MFA_CODE delivered via SMS.
  • PASSWORD_VERIFIER: The following challenge is to supply PASSWORD_CLAIM_SIGNATURE, PASSWORD_CLAIM_SECRET_BLOCK, and TIMESTAMP after the client-side SRP calculations.
  • CUSTOM_CHALLENGE: This is returned if your custom authentication flow determines that the user should pass another challenge before tokens are issued.
  • DEVICE_SRP_AUTH: If device tracking was enabled in your user pool, and the previous challenges were passed, this challenge is returned so that Amazon Cognito can start tracking the device.
  • DEVICE_PASSWORD_VERIFIER: This is similar to PASSWORD_VERIFIER, but for devices only.
  • ADMIN_NO_SRP_AUTH: This is returned if you need to authenticate with USERNAME and PASSWORD directly. An app client must be enabled to use this flow.
  • NEW_PASSWORD_REQUIRED: This is for users who are required to change their passwords after a successful first login. This challenge should be passed with NEW_PASSWORD and any other required attributes.

In this example, we are going to respond to the ‘new password required’ challenge type, so we are going to send the username, the previous password and the new password as the challenge response.

public  SpringSecurityUser signIn(AuthenticationRequestauthenticationRequest){
    AuthenticationResultType authenticationResult = null;
    AWSCognitoIdentityProvider cognitoClient = getAmazonCognitoIdentityClient();

    final Map<String, String>authParams = new HashMap<>();
    authParams.put(USERNAME, authenticationRequest.getUsername());  
    authParams.put(PASS_WORD, authenticationRequest.getPassword());

   final AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest();
       authRequest.withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH)
       .withClientId(cognitoConfig.getClientId())
       .withUserPoolId(cognitoConfig.getUserPoolId())
       withAuthParameters(authParams);

   AdminInitiateAuthResult result = cognitoClient.adminInitiateAuth(authRequest);

   //Has a Challenge
   if(StringUtils.isNotBlank(result.getChallengeName())) {
//If the challenge is required new Password validates if it has the new password variable.
   if(NEW_PASS_WORD_REQUIRED.equals(result.getChallengeName())){
      if(null == authenticationRequest.getNewPassword()) {
throw new CognitoException(messages.get(USER_MUST_PROVIDE_A_NEW_PASS_WORD), CognitoException.USER_MUST_CHANGE_PASS_WORD_EXCEPTION_CODE, result.getChallengeName());
      }else{
       //we still need the username
       final Map<String, String> challengeResponses = new HashMap<>();
       challengeResponses.put(USERNAME, authenticationRequest.getUsername());
       challengeResponses.put(PASS_WORD, authenticationRequest.getPassword());
       //add the new password to the params map
       challengeResponses.put(NEW_PASS_WORD, authenticationRequest.getNewPassword());
       //populate the challenge response
        final AdminRespondToAuthChallengeRequest request =
new AdminRespondToAuthChallengeRequest();
                                           request.withChallengeName(ChallengeNameType.NEW_PASSWORD_REQUIRED)
          .withChallengeResponses(challengeResponses)
          .withClientId(cognitoConfig.getClientId())
          .withUserPoolId(cognitoConfig.getPoolId())
          .withSession(result.getSession());

      AdminRespondToAuthChallengeResult resultChallenge =  
               cognitoClient.adminRespondToAuthChallenge(request);
      authenticationResult = resultChallenge.getAuthenticationResult();
      }
  }else{
    //has another challenge
    throw new CognitoException(result.getChallengeName(),    
  CognitoException.USER_MUST_DO_ANOTHER_CHALLENGE, result.getChallengeName());
  }
   }else{
       //Doesn't have a challenge
       authenticationResult = result.getAuthenticationResult();
   }
   cognitoClient.shutdown();
   return userAuthenticated;
}

Add User to Group:

This method adds a specific user to a specific group. The request parameters for the ‘Admin Add User to Group’ request are the group name, the username and user pool ID.

public void addUserToGroup(String username, String groupname){

   AWSCognitoIdentityProvider cognitoClient = getAmazonCognitoIdentityClient();
   AdminAddUserToGroupRequestaddUserToGroupRequest = new AdminAddUserToGroupRequest()
              .withGroupName(groupname)
              .withUserPoolId(cognitoConfig.getPoolId())
              .withUsername(username);

   cognitoClient.adminAddUserToGroup(addUserToGroupRequest);

   cognitoClient.shutdown();

                             
}

Change User Password:

This method changes the password for a specific user in a user pool. The request parameters are the access token we received while doing the sign in, the previous password, and the proposed password.

public void changePassword(PasswordRequest passwordRequest) {

     AWSCognitoIdentityProvider cognitoClient= getAmazonCognitoIdentityClient();
     ChangePasswordRequest changePasswordRequest= newChangePasswordRequest()
              .withAccessToken(passwordRequest.getAccessToken())
              .withPreviousPassword(passwordRequest.getOldPassword())
              .withProposedPassword(passwordRequest.getPassword());

      cognitoClient.changePassword(changePasswordRequest);
      cognitoClient.shutdown();

}

Get User Info:

This method retrieves all the user attributes for a specific user in a user pool as an administrator. The request parameters for ’Admin Get User’ are the username and the user pool ID. If the action is successful, it returns the user attributes, the preferred MFA settings, MFA options, and a flag indicating whether the user is enabled or not.

public UserResponse getUserInfo(String username) {

       AWSCognitoIdentityProvider cognitoClient = getAmazonCognitoIdentityClient();             
AdminGetUserRequest userRequest = new AdminGetUserRequest()
                      .withUsername(username)
                      .withUserPoolId(cognitoConfig.getUserPoolId());


       AdminGetUserResult userResult = cognitoClient.adminGetUser(userRequest);

       UserResponse userResponse = new UserResponse();
       userResponse.setUsername(userResult.getUsername());
       userResponse.setUserStatus(userResult.getUserStatus());
       userResponse.setUserCreateDate(userResult.getUserCreateDate());
       userResponse.setLastModifiedDate(userResult.getUserLastModifiedDate());

       List userAttributes = userResult.getUserAttributes();
       for(AttributeTypeattribute: userAttributes) {
              if(attribute.getName().equals("custom:companyName")) {
                 userResponse.setCompanyName(attribute.getValue());
}else if(attribute.getName().equals("custom:companyPosition")) {
                 userResponse.setCompanyPosition(attribute.getValue());
              }else if(attribute.getName().equals("email")) {
                 userResponse.setEmail(attribute.getValue());
              }
       }

        cognitoClient.shutdown();
       return userResponse;
              
}

Authentication and Authorization Flow:

Once we have signed in to Amazon Cognito, it returns 3 JSON Web Tokens: the token ID, the access token, and the refresh token. In this part, I’m going to explain how we can use the token ID as a bearer access token in our Java Web Application.

Authentication Flow

Flow details:

  1. The client authenticates against a user pool.
  2. The user pool assigns 3 JSON Web Tokens (JWT) — ID, access and refresh —  to the client.
  3. The ID JSON Web Token is passed to the identity pool, and a role is chosen via the JWT claims. The user receives IAM temporary credentials with privileges that are based on the IAM role that was mapped to the group that the user belongs to.
  4. Then, the user can make calls to other services on AWS and applications such as databases, as shown in the image. These privileges are dictated by IAM policies.

 

What is OAuth 2.0?

OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on a Web API. Web APIs protected by OAuth 2.0 need to validate access tokens, which can be implemented as signed JSON Web Tokens.

For this implementation, we are going to use the Maven Nimbus Jose+JWT library dependency by adding the following lines to our pom.xml

com.nimbusds nimbus-jose-jwt 4.23

The Nimbus Jose+JWT library provides a framework for all the steps to validate a JSON Web Token. The JWT validation steps are:

  1.    JWT Parsing: The access token provided is parsed as a JWT. If the parsing fails, the token will be considered invalid.
  2.    Algorithm Check: The JSON Web Key algorithm specified in the JSON Web Token header is checked. If a token with an unexpected algorithm is received, the token will be immediately rejected.
  3.    Signature Check: In this step, the digital signature is verified.
  4.    JWT Claims Check:The JSON Web Token claims set is validated; to verify JWT claims, the following steps are necessary:
    1.    Verify that the token has not expired.
    2.    The audience (aud) claim should match the app client ID created in the Amazon Cognito User Pool.
    3.     The issuer (iss) claim should match the user pool. For example, a user pool created in the selected region (us-east-1) has an ‘iss’ value of: https://cognito-idp.us-east-1.amazonaws.com/<userpoolID>.
    4.    Check the token_use claim.
    5.     If you are only accepting the access token in your Web APIs, its value must be ‘access.’
    6.     If you are only using the ID token, its value must be ‘id.’
    7.    If you are using both ID and access tokens, the token_use claim must be either ‘id’ or ‘access.’
    8.    You can now trust the claims inside the token.

Implementation:

  1.   We create a ConfigurableJWTProcessor:
@Bean
   public ConfigurableJWTProcessor configurableJWTProcessor() throws MalformedURLException {
        ResourceRetriever resourceRetriever =
             new DefaultResourceRetriever(jwtConfiguration.getConnectionTimeout(),//2000
                  jwtConfiguration.getReadTimeout()//2000);
        //https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json.
        URL jwkSetURL= new URL(jwtConfiguration.getJwkUrl());
        //Creates the JSON Web Key (JWK)
        JWKSource keySource= new RemoteJWKSet(jwkSetURL, resourceRetriever);
        ConfigurableJWTProcessor jwtProcessor= new DefaultJWTProcessor();
        //RSASSA-PKCS-v1_5 using SHA-256 hash algorithm
        JWSKeySelector keySelector= new JWSVerificationKeySelector(RS256, keySource);
        jwtProcessor.setJWSKeySelector(keySelector);
        return jwtProcessor;
    }
  1.   We initialize the com.nimbusds.jwt.proc.ConfigurableJWTProcessor:

@Autowired       private ConfigurableJWTProcessor configurableJWTProcessor;

  1.   Then, we extract the access token from the Authentication Header of the request. In this case, we are going to use the Bearer JWT Access Token.
public Authentication getAuthentication(HttpServletRequest request) throws ParseException, BadJOSEException, JOSEException {
       String idToken = request.getHeader(jwtConfiguration.getHttpHeader());
       if(null == idToken) {
          throw new CognitoException(NO_TOKEN_FOUND,
CognitoException.NO_TOKEN_PROVIDED_EXCEPTION,
"No token found in Http Authorization Header");
       }else{
  idToken = extractAndDecodeJwt(idToken);
         JWTClaimsSetclaimsSet = null;
         claimsSet= configurableJWTProcessor.process(idToken, null);
         if (!isIssuedCorrectly(claimsSet)) {
           throw new CognitoException(INVALID_TOKEN,
                 CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
                 String.format("Issuer %s in JWT token doesn't match cognito idp %s",
                 claimsSet.getIssuer(),jwtConfiguration.getCognitoIdentityPoolUrl()));
         }

        if(!isIdToken(claimsSet)) {
           throw new CognitoException(INVALID_TOKEN,
                  CognitoException.NOT_A_TOKEN_EXCEPTION,
                  "JWT Token doesn't seem to be an ID Token");
       }

       String username = claimsSet.getClaims()
             .get(jwtConfiguration.getUserNameField()).toString();

       List groups = (List) claimsSet.getClaims()
                .get(jwtConfiguration.getGroupsField());
       List grantedAuthorities = convertList(groups, group-> new
                SimpleGrantedAuthority(ROLE_PREFIX+ group.toUpperCase()));
       User user = new User(username, EMPTY_STRING, grantedAuthorities);

       return new CognitoJwtAuthentication(user, claimsSet, grantedAuthorities);

       }
}

private boolean isIssuedCorrectly(JWTClaimsSet claimsSet) {
       return claimsSet.getIssuer().equals(jwtConfiguration.getCognitoIdentityPoolUrl());
}

private boolean isIdToken(JWTClaimsSet claimsSet) {
       return claimsSet.getClaim("token_use").equals("id");
}

Conclusions

By not implementing a user management service on the Cloud such as Amazon Cognito, a developer must go through the process of creating the user, passwords, roles, and access management platform, which consumes a lot of time and does not necessarily contribute greater value to the client’s final solution.

Amazon Cognito is a fully managed service that scales to millions of users by assigning them to standards-based groups such as OAuth 2.0, SAML 2.0, and OpenID Connect. This allows us to have full control of the user management in our Java application without writing any backend code or managing any type of infrastructure. As a result of this, project development time is improved and the developer is able to focus on the business logic of the application to be developed.

Ready to be Unstoppable? Partner with Gorilla Logic, and you can be.

TALK TO OUR SALES TEAM