/** \file * * Oct 9, 2018 * * Copyright Ian Kaplan 2018 * * @author Ian Kaplan, www.bearcave.com, [email protected] */ package cognito_demo.services; import java.util.HashMap; import java.util.List; import java.util.Map; import com.amazonaws.SdkBaseException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; 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.AWSCognitoIdentityProviderException; import com.amazonaws.services.cognitoidp.model.AdminCreateUserRequest; import com.amazonaws.services.cognitoidp.model.AdminDeleteUserRequest; import com.amazonaws.services.cognitoidp.model.AdminGetUserRequest; import com.amazonaws.services.cognitoidp.model.AdminGetUserResult; import com.amazonaws.services.cognitoidp.model.AdminInitiateAuthRequest; import com.amazonaws.services.cognitoidp.model.AdminInitiateAuthResult; import com.amazonaws.services.cognitoidp.model.AdminRespondToAuthChallengeRequest; import com.amazonaws.services.cognitoidp.model.AdminRespondToAuthChallengeResult; import com.amazonaws.services.cognitoidp.model.AdminUpdateUserAttributesRequest; import com.amazonaws.services.cognitoidp.model.AdminUserGlobalSignOutRequest; import com.amazonaws.services.cognitoidp.model.AttributeType; import com.amazonaws.services.cognitoidp.model.AuthFlowType; import com.amazonaws.services.cognitoidp.model.AuthenticationResultType; import com.amazonaws.services.cognitoidp.model.ChallengeNameType; import com.amazonaws.services.cognitoidp.model.ChangePasswordRequest; import com.amazonaws.services.cognitoidp.model.ChangePasswordResult; import com.amazonaws.services.cognitoidp.model.CodeDeliveryDetailsType; import com.amazonaws.services.cognitoidp.model.ConfirmForgotPasswordRequest; import com.amazonaws.services.cognitoidp.model.ConfirmForgotPasswordResult; import com.amazonaws.services.cognitoidp.model.ForgotPasswordRequest; import com.amazonaws.services.cognitoidp.model.ForgotPasswordResult; import com.amazonaws.services.cognitoidp.model.ListUsersRequest; import com.amazonaws.services.cognitoidp.model.ListUsersResult; import com.amazonaws.services.cognitoidp.model.UserType; /** * <h4> * AuthenticationService * </h4> * <p> * Provide authentication and other user management services. * </p> * <p> * Oct 16, 2018 * </p> * * @author Ian Kaplan, [email protected] */ public class AuthenticationService implements AuthenticationInterface, CognitoResources { private final static String USERNAME = "USERNAME"; private final static String PASSWORD = "PASSWORD"; private final static String NEW_PASSWORD = "NEW_PASSWORD"; protected static AWSCognitoIdentityProvider mIdentityProvider = null; public AuthenticationService() { if (mIdentityProvider == null) { mIdentityProvider = getAmazonCognitoIdentityClient(); } } /** * <p> * Build an AWSCredentials object from the ID and secret key. * </p> * * @param AWS_ID * @param AWS_KEY * @return an AWSCredentials object, initialized with the ID and Key. */ protected AWSCredentials getCredentials(String AWS_ID, String AWS_KEY) { AWSCredentials credentials = new BasicAWSCredentials( AWS_ID, AWS_KEY ); return credentials; } /** * <p> * Build an AWS cognito identity provider, based on the parameters defined in the CognitoResources interface. * </p> * * @return */ protected AWSCognitoIdentityProvider getAmazonCognitoIdentityClient() { AWSCredentials credentials = getCredentials(cognitoID, cognitoKey); AWSCredentialsProvider credProvider = new AWSStaticCredentialsProvider( credentials ); AWSCognitoIdentityProvider client = AWSCognitoIdentityProviderClientBuilder.standard() .withCredentials(credProvider) .withRegion(region) .build(); return client; } /** * <p> * Create a new user. * </p> * <p> * When a user is created in the Cognito user pool, the user account will be initially inactive. An email with a * temporary password will be sent to the user's email address. The user will use this temporary password to login * (via the login page). They will then be redirected to the change password page. * </p> * <p> * This function assumes that the user does not exist (the user name and email should be unique). * </p> * <p> * The caller should check whether the user already exists and handle that with the appropriate logic. * If the user already exists, this method will throw an exception. * </p> * <p> * The email_verified attribute (set to true) is very important. Without setting this attribute, the email address will * be treated as unverified. If the email is not verified, an attempt to reset the password will result in an unverified * email error. * </P> * * @param userName The publicly visible user name * @param emailAddress the user's email address. This will be the email address that the temporary password will be sent to. * @param location The users location (e.g., Bonaire, Aruba, Curacao) * @return a UserType object. * * @throws AWSCognitoIdentityProviderException This is the parent exception for Cognito errors. The caller should * handle the UsernameExistsException error. */ @Override public void createNewUser(final UserInfo userInfo) throws AWSCognitoIdentityProviderException { final String emailAddr = userInfo.getEmailAddr(); if (emailAddr != null && emailAddr.length() > 0) { // There should be no user with this email address, so info should be null UserInfo info = findUserByEmailAddr( emailAddr ); if (info == null) { AdminCreateUserRequest cognitoRequest = new AdminCreateUserRequest() .withUserPoolId(poolID) .withUsername(userInfo.getUserName()) .withUserAttributes( new AttributeType() .withName(EMAIL) .withValue(emailAddr), new AttributeType() .withName(LOCATION) .withValue(userInfo.getLocation()), new AttributeType() .withName("email_verified") .withValue("true") ); // The AdminCreateUserResult resturned by this function doesn't contain useful information so the // result is ignored. mIdentityProvider.adminCreateUser(cognitoRequest); } else { // The caller should have checked that the email address is not already used by a user. If this is not // done, then it's an exception (e.g., something is wrong). throw new DuplicateEmailException("The email address " + emailAddr + " is already in the database" ); } } } // createNewUser /** Delete an existing user. This code assumes that the operation is logically permitted (a user is only allowed to delete their own account, after password verification). */ @Override public void deleteUser(final String userName, final String password) throws AWSCognitoIdentityProviderException { SessionInfo sessionInfo = sessionLogin(userName, password); if (sessionInfo != null) { AdminDeleteUserRequest deleteRequest = new AdminDeleteUserRequest() .withUsername(userName) .withUserPoolId(poolID); // the adminDeleteUserRequest returns an AdminDeleteUserResult which doesn't contain anything useful. // So the result is ignored. mIdentityProvider.adminDeleteUser(deleteRequest); } } /** Update user attributes associated with the user information stored in Cognito. Note that the email attribute is updated separately. */ @Override public void updateUserAttributes(final UserInfo newInfo) throws AWSCognitoIdentityProviderException { AdminUpdateUserAttributesRequest updateRequest = new AdminUpdateUserAttributesRequest() .withUsername(newInfo.getUserName()) .withUserPoolId(poolID) .withUserAttributes( new AttributeType() .withName(LOCATION) .withValue(newInfo.getLocation()) ); mIdentityProvider.adminUpdateUserAttributes(updateRequest); } /** * <blockquote> * After a successful authentication, Amazon Cognito returns user pool tokens to your app. * </blockquote> * <blockquote> * The ID Token contains claims about the identity of the authenticated user such as name, email, and phone_number. * </blockquote> * * @param userName * @param password * @return * @throws AWSCognitoIdentityProviderException */ protected SessionInfo sessionLogin(final String userName, final String password) throws AWSCognitoIdentityProviderException { SessionInfo info = null; HashMap<String, String> authParams = new HashMap<String, String>(); authParams.put("USERNAME", userName); authParams.put("PASSWORD", password); AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest() .withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH) .withUserPoolId(poolID) .withClientId(clientID) .withAuthParameters(authParams); AdminInitiateAuthResult authResult = mIdentityProvider.adminInitiateAuth(authRequest); // If there is a bad username the adminInitiateAuth() call will throw a UserNotFoundException. // Unfortunately the AWS documentation doesn't say what happens if the password is incorrect. // Perhaps the NotAuthorizedException is thrown? if (authResult != null) { final String session = authResult.getSession(); String accessToken = null; AuthenticationResultType resultType = authResult.getAuthenticationResult(); if (resultType != null) { accessToken = resultType.getAccessToken(); } final String challengeResult = authResult.getChallengeName(); info = new SessionInfo(session, accessToken, challengeResult ); } return info; } /** * <p> * Log a user into the system using Cognito. * </p> * @param userName * @param password * @return a LoginInfo object that includes the access token which is used for user operations like change password. * @throws AWSCognitoIdentityProviderException In addition to various other exceptions, the function will throw * the UserNotFoundException if the user is not found in Cognito's database. */ @Override public LoginInfo userLogin(final String userName, final String password) throws AWSCognitoIdentityProviderException { LoginInfo loginInfo = null; SessionInfo sessionInfo = sessionLogin(userName, password); // The process of sessionLogin should either return a session ID (if the account has not been verified) or a // token ID (if the account has been verified). if (sessionInfo != null) { UserInfo userInfo = getUserInfo(userName); loginInfo = new LoginInfo( userInfo ); // check to see if the password used was a temporary password. If this is the case, the password // must be reset. String challengeResult = sessionInfo.getChallengeResult(); if (challengeResult != null && challengeResult.length() > 0) { loginInfo.setNewPasswordRequired( challengeResult.equals(ChallengeNameType.NEW_PASSWORD_REQUIRED.name() )); } } return loginInfo; } /** * <p> * Log the user out. * </p> * @param userName * @throws AWSCognitoIdentityProviderException */ @Override public void userLogout(final String userName) throws AWSCognitoIdentityProviderException { AdminUserGlobalSignOutRequest signOutRequest = new AdminUserGlobalSignOutRequest() .withUsername(userName) .withUserPoolId(poolID); // The AdminUserGlobalSignOutResult returned by this function does not contain any useful information so the // result is ignored. mIdentityProvider.adminUserGlobalSignOut(signOutRequest); } /** * <p> * Change the password for a logged in user. Unlike the forgotten password logic, this does not require an * emailed code to change the password. * </p> * @throws NotAuthorizedException (subclass of AWSCognitoIdentityProviderException) if the password is wrong. */ @Override public void changePassword(final PasswordRequest passwordRequest) throws AWSCognitoIdentityProviderException { // Signin with the old/temporary password. Apparently this is needed to establish a session for the // password change. final SessionInfo sessionInfo = sessionLogin(passwordRequest.getUserName(), passwordRequest.getOldPassword()); if (sessionInfo != null && sessionInfo.getAccessToken() != null) { ChangePasswordRequest changeRequest = new ChangePasswordRequest() .withAccessToken( sessionInfo.getAccessToken()) .withPreviousPassword( passwordRequest.getOldPassword()) .withProposedPassword( passwordRequest.getNewPassword()); ChangePasswordResult rslt = mIdentityProvider.changePassword(changeRequest); } else { String msg = "Access token was not returned from session login"; throw new AWSCognitoIdentityProviderException( msg ); } } /** * <p> * Change user's email address. The email address is a special attribute since it will be used for communication * with the user. * </p> * <p> * In this code the user is allowed to change their email addressed without verification via an emailed code * by setting the "email_verified" attribute to true. * </p> * <p> * Ideally it would be nice to verify the email address via an emailed code, since this assures that the email address * is valid and belongs to the user. However, I have been unable to figure out how to do this within the Cognito framework. * So we assume that the user entered a correct email address. They can always change it if an error is discovered. * </p> */ @Override public void changeEmail(final String userName, final String newEmailAddr) throws AWSCognitoIdentityProviderException { AdminUpdateUserAttributesRequest updateRequest = new AdminUpdateUserAttributesRequest() .withUsername(userName) .withUserPoolId(poolID) .withUserAttributes( new AttributeType() .withName(EMAIL) .withValue(newEmailAddr), new AttributeType() .withName("email_verified") .withValue("true") ); mIdentityProvider.adminUpdateUserAttributes(updateRequest); } /** * <p> * Change the user's password from a temporary password to a new (permanent) password. * </p> * <p> * This function is called to set the password using the temporary password emailed to the user's email * address. * </p> * * @param passwordRequest a PasswordRequest object that provides the information needed to change the password. * @throws AWSCognitoIdentityProviderException the InvalidPasswordException will be thrown if the user provides * an incorrect old password. */ @Override public void changeFromTemporaryPassword(final PasswordRequest passwordRequest) throws AWSCognitoIdentityProviderException { // Signin with the old/temporary password. Apparently this is needed to establish a session for the // password change. final SessionInfo sessionInfo = sessionLogin(passwordRequest.getUserName(), passwordRequest.getOldPassword()); final String sessionString = sessionInfo.getSession(); if (sessionString != null && sessionString.length() > 0) { Map<String, String> challengeResponses = new HashMap<String, String>(); challengeResponses.put(USERNAME, passwordRequest.getUserName()); challengeResponses.put(PASSWORD, passwordRequest.getOldPassword()); challengeResponses.put(NEW_PASSWORD, passwordRequest.getNewPassword()); AdminRespondToAuthChallengeRequest changeRequest = new AdminRespondToAuthChallengeRequest() .withChallengeName( ChallengeNameType.NEW_PASSWORD_REQUIRED) .withChallengeResponses(challengeResponses) .withClientId(clientID) .withUserPoolId(poolID) .withSession( sessionString ); AdminRespondToAuthChallengeResult challengeResponse = mIdentityProvider.adminRespondToAuthChallenge(changeRequest); } } // changePassword /** * <p> * Reset the user's password using a code that they received via email. * </p> * @param resetRequest */ @Override public void resetPassword(ResetPasswordRequest resetRequest) throws AWSCognitoIdentityProviderException { ConfirmForgotPasswordRequest passwordRequest = new ConfirmForgotPasswordRequest() .withUsername(resetRequest.getUserName()) .withConfirmationCode(resetRequest.getResetCode()) .withClientId(clientID) .withPassword(resetRequest.getNewPassword()); ConfirmForgotPasswordResult rslt = mIdentityProvider.confirmForgotPassword(passwordRequest); } /** * <p> * This function is called to reset the user's password if the password has been forgotten. The function will * result in an email being sent to the user's email account with a reset code. * </p> * @param userName the user name for the account that should be reset. */ @Override public void forgotPassword(final String userName) throws AWSCognitoIdentityProviderException { ForgotPasswordRequest passwordRequest = new ForgotPasswordRequest() .withClientId(clientID) .withUsername(userName); ForgotPasswordResult rslt = mIdentityProvider.forgotPassword(passwordRequest); CodeDeliveryDetailsType delivery = rslt.getCodeDeliveryDetails(); } /** * <p> * Get the information associated with the user. * </p> * * @param userName the name of the user to be returned * @return a UserInfo object if the information could be retreived, null otherwise. * @throws AWSCognitoIdentityProviderException if the user didn't exist, then the UserNotFoundException exception * will be thrown. The other exceptions that are subclasses of AWSCognitoIdentityProviderException are * authorization or internal errors. */ @Override public UserInfo getUserInfo(final String userName) throws AWSCognitoIdentityProviderException { AdminGetUserRequest userRequest = new AdminGetUserRequest() .withUsername(userName) .withUserPoolId(poolID); AdminGetUserResult userResult = mIdentityProvider.adminGetUser(userRequest); List<AttributeType> userAttributes = userResult.getUserAttributes(); final String rsltUserName = userResult.getUsername(); String emailAddr = null; String location = null; for (AttributeType attr : userAttributes) { if (attr.getName().equals(EMAIL)) { emailAddr = attr.getValue(); } else if (attr.getName().equals(LOCATION)) { location = attr.getValue(); } } UserInfo info = null; if (rsltUserName != null && emailAddr != null && location != null) { info = new UserInfo(rsltUserName, emailAddr, location); } return info; } // getUserInfo /** * <p> * Find a user by their email address. * </p> * @return a UserInfo object if the lookup succeeded or null if there was no user associated with that email * address. */ @Override public UserInfo findUserByEmailAddr(String email) throws AWSCognitoIdentityProviderException { UserInfo info = null; if (email != null && email.length() > 0) { final String emailQuery = "email=\"" + email + "\""; ListUsersRequest usersRequest = new ListUsersRequest() .withUserPoolId(poolID) .withAttributesToGet(EMAIL, LOCATION) .withFilter(emailQuery); ListUsersResult usersRslt = mIdentityProvider.listUsers(usersRequest); List<UserType> users = usersRslt.getUsers(); if (users != null && users.size() > 0) { // There should only be a single instance of an email address in the Cognito database // (e.g., there should not be multiple users with the same email address). if (users.size() == 1) { UserType user = users.get(0); final String userName = user.getUsername(); String emailAddr = null; String location = null; List<AttributeType> attributes = user.getAttributes(); if (attributes != null) { for (AttributeType attr : attributes) { if (attr.getName().equals(EMAIL)) { emailAddr = attr.getValue(); } else if (attr.getName().equals(LOCATION)) { location = attr.getValue(); } } if (userName != null && emailAddr != null && location != null) { info = new UserInfo(userName, emailAddr, location); } } } else { throw new DuplicateEmailException("More than one user has the email address " + email); } } } return info; } /** * <p> * Determine whether a user with userName exists in the login database. * </p> * * @param userName * @return true if the user exists, false otherwise. */ @Override public boolean hasUser(final String userName) { boolean userExists = false; try { UserInfo info = getUserInfo( userName ); if (info != null && info.getUserName() != null && info.getUserName().length() > 0 && info.getUserName().equals(userName)) { userExists = true; } } catch (SdkBaseException ex) {} return userExists; } }