Java tutorial
/** * Copyright 2014 Stephen Cummins & Nick Rogers. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package uk.ac.cam.cl.dtg.segue.api.managers; import static uk.ac.cam.cl.dtg.segue.api.Constants.*; import java.io.IOException; import java.net.URI; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.*; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import ma.glasnost.orika.MapperFacade; import ma.glasnost.orika.impl.DefaultMapperFactory; import org.apache.commons.lang3.Validate; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.cam.cl.dtg.segue.api.Constants; import uk.ac.cam.cl.dtg.segue.auth.AuthenticationProvider; import uk.ac.cam.cl.dtg.segue.auth.IAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.IPasswordAuthenticator; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationCodeException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationProviderMappingException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticatorSecurityException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.CodeExchangeException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.CrossSiteRequestForgeryException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.DuplicateAccountException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.IncorrectCredentialsProvidedException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidPasswordException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidTokenException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.MissingRequiredFieldException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoCredentialsAvailableException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserException; import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException; import uk.ac.cam.cl.dtg.segue.comm.CommunicationException; import uk.ac.cam.cl.dtg.segue.comm.EmailManager; import uk.ac.cam.cl.dtg.segue.comm.EmailMustBeVerifiedException; import uk.ac.cam.cl.dtg.segue.comm.EmailType; import uk.ac.cam.cl.dtg.segue.dao.ILogManager; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; import uk.ac.cam.cl.dtg.segue.dao.users.IUserDataManager; import uk.ac.cam.cl.dtg.segue.dos.users.AnonymousUser; import uk.ac.cam.cl.dtg.segue.dos.users.EmailVerificationStatus; import uk.ac.cam.cl.dtg.segue.dos.users.RegisteredUser; import uk.ac.cam.cl.dtg.segue.dos.users.Role; import uk.ac.cam.cl.dtg.segue.dos.users.UserFromAuthProvider; import uk.ac.cam.cl.dtg.segue.dto.users.AbstractSegueUserDTO; import uk.ac.cam.cl.dtg.segue.dto.users.AnonymousUserDTO; import uk.ac.cam.cl.dtg.segue.dto.users.RegisteredUserDTO; import uk.ac.cam.cl.dtg.segue.dto.users.UserSummaryDTO; import uk.ac.cam.cl.dtg.segue.dto.users.DetailedUserSummaryDTO; import uk.ac.cam.cl.dtg.util.PropertiesLoader; import com.google.api.client.util.Lists; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; /** * This class is responsible for managing all user data and orchestration of calls to a user Authentication Manager for * dealing with sessions and passwords. */ public class UserAccountManager { private static final Logger log = LoggerFactory.getLogger(UserAccountManager.class); private final IUserDataManager database; private final QuestionManager questionAttemptDb; private final ILogManager logManager; private final MapperFacade dtoMapper; private final EmailManager emailManager; private final Cache<String, AnonymousUser> temporaryUserCache; private final Map<AuthenticationProvider, IAuthenticator> registeredAuthProviders; private final UserAuthenticationManager userAuthenticationManager; private final PropertiesLoader properties; /** * Create an instance of the user manager class. * * @param database * - an IUserDataManager that will support persistence. * @param questionDb * - allows this class to instruct the questionDB to merge an anonymous user with a registered user. * @param properties * - A property loader * @param providersToRegister * - A map of known authentication providers. * @param dtoMapper * - the preconfigured DO to DTO object mapper for user objects. * @param emailQueue * - the preconfigured communicator manager for sending e-mails. * @param logManager * - so that we can log events for users. * @param userAuthenticationManager * - Class responsible for handling sessions, passwords and linked accounts. */ @Inject public UserAccountManager(final IUserDataManager database, final QuestionManager questionDb, final PropertiesLoader properties, final Map<AuthenticationProvider, IAuthenticator> providersToRegister, final MapperFacade dtoMapper, final EmailManager emailQueue, final ILogManager logManager, final UserAuthenticationManager userAuthenticationManager) { this(database, questionDb, properties, providersToRegister, dtoMapper, emailQueue, CacheBuilder.newBuilder().expireAfterAccess(ANONYMOUS_SESSION_DURATION_IN_MINUTES, TimeUnit.MINUTES) .<String, AnonymousUser>build(), logManager, userAuthenticationManager); } /** * Fully injectable constructor. * * @param database * - an IUserDataManager that will support persistence. * @param questionDb * - supports persistence of question attempt info. * @param properties * - A property loader * @param providersToRegister * - A map of known authentication providers. * @param dtoMapper * - the preconfigured DO to DTO object mapper for user objects. * @param emailQueue * - the preconfigured communicator manager for sending e-mails. * @param temporaryUserCache * - the preconfigured communicator manager for sending e-mails. * @param logManager * - so that we can log events for users.. * @param userAuthenticationManager * - Class responsible for handling sessions, passwords and linked accounts. */ public UserAccountManager(final IUserDataManager database, final QuestionManager questionDb, final PropertiesLoader properties, final Map<AuthenticationProvider, IAuthenticator> providersToRegister, final MapperFacade dtoMapper, final EmailManager emailQueue, final Cache<String, AnonymousUser> temporaryUserCache, final ILogManager logManager, final UserAuthenticationManager userAuthenticationManager) { Validate.notNull(properties.getProperty(HMAC_SALT)); Validate.notNull(Integer.parseInt(properties.getProperty(SESSION_EXPIRY_SECONDS))); Validate.notNull(properties.getProperty(HOST_NAME)); this.properties = properties; this.database = database; this.questionAttemptDb = questionDb; this.temporaryUserCache = temporaryUserCache; this.logManager = logManager; this.registeredAuthProviders = providersToRegister; this.dtoMapper = dtoMapper; this.emailManager = emailQueue; this.userAuthenticationManager = userAuthenticationManager; } /** * This method will start the authentication process and ultimately provide a url for the client to redirect the * user to. This url will be for a 3rd party authenticator who will use the callback method provided after they have * authenticated. * * Users who are already logged already will be returned their UserDTO without going through the authentication * process. * * @param request * - http request that we can attach the session to and save redirect url in. * @param provider * - the provider the user wishes to authenticate with. * @return a URI for redirection * @throws IOException - * @throws AuthenticationProviderMappingException - as per exception description. */ public URI authenticate(final HttpServletRequest request, final String provider) throws IOException, AuthenticationProviderMappingException { return this.userAuthenticationManager.getThirdPartyAuthURI(request, provider); } /** * This method will start the authentication process for linking a user to a 3rd party provider. It will ultimately * provide a url for the client to redirect the user to. This url will be for a 3rd party authenticator who will use * the callback method provided after they have authenticated. * * Users must already be logged in to use this method otherwise a 401 will be returned. * * @param request * - http request that we can attach the session to. * @param provider * - the provider the user wishes to authenticate with. * @return A redirection URI - also this endpoint ensures that the request has a session attribute on so we know * that this is a link request not a new user. * @throws IOException - * @throws AuthenticationProviderMappingException - as per exception description. */ public URI initiateLinkAccountToUserFlow(final HttpServletRequest request, final String provider) throws IOException, AuthenticationProviderMappingException { // record our intention to link an account. request.getSession().setAttribute(LINK_ACCOUNT_PARAM_NAME, Boolean.TRUE); return this.userAuthenticationManager.getThirdPartyAuthURI(request, provider); } /** * Authenticate Callback will receive the authentication information from the different provider types. (e.g. OAuth * 2.0 (IOAuth2Authenticator) or bespoke) * * This method will either register a new user and attach the linkedAccount or locate the existing account of the * user and create a session for that. * * @param request * - http request from the user - should contain url encoded token details. * @param response * to store the session in our own segue cookie. * @param provider * - the provider who has just authenticated the user. * @return Response containing the user object. Alternatively a SegueErrorResponse could be returned. * @throws AuthenticationProviderMappingException * - if we cannot locate an appropriate authenticator. * @throws SegueDatabaseException * - if there is a local database error. * @throws IOException * - Problem reading something * @throws NoUserException * - If the user doesn't exist with the provider. * @throws AuthenticatorSecurityException * - If there is a security probably with the authenticator. * @throws CrossSiteRequestForgeryException * - as per exception description. * @throws CodeExchangeException * - as per exception description. * @throws AuthenticationCodeException * - as per exception description. */ public RegisteredUserDTO authenticateCallback(final HttpServletRequest request, final HttpServletResponse response, final String provider) throws AuthenticationProviderMappingException, AuthenticatorSecurityException, NoUserException, IOException, SegueDatabaseException, AuthenticationCodeException, CodeExchangeException, CrossSiteRequestForgeryException { IAuthenticator authenticator = this.userAuthenticationManager.mapToProvider(provider); // get the auth provider user data. UserFromAuthProvider providerUserDO = this.userAuthenticationManager.getThirdPartyUserInformation(request, provider); // if the UserFromAuthProvider exists then this is a login request so process it. RegisteredUser userFromLinkedAccount = this.userAuthenticationManager.getSegueUserFromLinkedAccount( authenticator.getAuthenticationProvider(), providerUserDO.getProviderUserId()); if (userFromLinkedAccount != null) { return this.logUserIn(request, response, userFromLinkedAccount); } RegisteredUser currentUser = getCurrentRegisteredUserDO(request); // if the user is currently logged in and this is a request for a linked account, then create the new link. if (null != currentUser) { Boolean intentionToLinkRegistered = (Boolean) request.getSession() .getAttribute(LINK_ACCOUNT_PARAM_NAME); if (intentionToLinkRegistered == null || !intentionToLinkRegistered) { throw new SegueDatabaseException("User is already authenticated - " + "expected request to link accounts but none was found."); } List<AuthenticationProvider> usersProviders = this.database .getAuthenticationProvidersByUser(currentUser); if (!usersProviders.contains(authenticator.getAuthenticationProvider())) { // create linked account this.userAuthenticationManager.linkProviderToExistingAccount(currentUser, authenticator.getAuthenticationProvider(), providerUserDO); // clear link accounts intention until next time request.removeAttribute(LINK_ACCOUNT_PARAM_NAME); } return this.convertUserDOToUserDTO(getCurrentRegisteredUserDO(request)); } else { // this must be a registration request RegisteredUser segueUserDO = this .registerUserWithFederatedProvider(authenticator.getAuthenticationProvider(), providerUserDO); RegisteredUserDTO segueUserDTO = this.logUserIn(request, response, segueUserDO); segueUserDTO.setFirstLogin(true); try { ImmutableMap<String, Object> emailTokens = ImmutableMap.of("provider", provider.toLowerCase()); emailManager.sendTemplatedEmailToUser(segueUserDTO, emailManager.getEmailTemplateDTO("email-template-registration-confirmation-federated"), emailTokens, EmailType.SYSTEM); } catch (ContentManagerException e) { log.error("Registration email could not be sent due to content issue: " + e.getMessage()); } return segueUserDTO; } } /** * This method will attempt to authenticate the user using the provided credentials and if successful will log the * user in and create a session. * * @param request * - http request that we can attach the session to. * @param response * to store the session in our own segue cookie. * @param provider * - the provider the user wishes to authenticate with. * @param email * - the email address of the account holder. * @param password * - the plain text password. * @return A response containing the UserDTO object or a SegueErrorResponse. * @throws AuthenticationProviderMappingException * - if we cannot find an authenticator * @throws IncorrectCredentialsProvidedException * - if the password is incorrect * @throws NoUserException * - if the user does not exist * @throws NoCredentialsAvailableException * - If the account exists but does not have a local password * @throws SegueDatabaseException * - if there is a problem with the database. */ public final RegisteredUserDTO authenticateWithCredentials(final HttpServletRequest request, final HttpServletResponse response, final String provider, final String email, final String password) throws AuthenticationProviderMappingException, IncorrectCredentialsProvidedException, NoUserException, NoCredentialsAvailableException, SegueDatabaseException { Validate.notBlank(email); Validate.notBlank(password); // get the current user based on their session id information. RegisteredUserDTO currentUser = this.convertUserDOToUserDTO(this.getCurrentRegisteredUserDO(request)); if (null != currentUser) { return currentUser; } RegisteredUser user = this.userAuthenticationManager.getSegueUserFromCredentials(provider, email, password); return this.logUserIn(request, response, user); } /** * Utility method to ensure that the credentials provided are the current correct ones. If they are invalid an * exception will be thrown otherwise nothing will happen. * * @param provider * - the password provider who will validate the credentials. * @param email * - the email address of the account holder. * @param password * - the plain text password. * @throws AuthenticationProviderMappingException * - if we cannot find an authenticator * @throws IncorrectCredentialsProvidedException * - if the password is incorrect * @throws NoUserException * - if the user does not exist * @throws NoCredentialsAvailableException * - If the account exists but does not have a local password * @throws SegueDatabaseException * - if there is a problem with the database. */ public void ensureCorrectPassword(final String provider, final String email, final String password) throws AuthenticationProviderMappingException, IncorrectCredentialsProvidedException, NoUserException, NoCredentialsAvailableException, SegueDatabaseException { // this method will throw an error if the credentials are incorrect. this.userAuthenticationManager.getSegueUserFromCredentials(provider, email, password); } /** * Unlink User From AuthenticationProvider * * Removes the link between a user and a provider. * * @param user * - user to affect. * @param providerString * - provider to unassociated. * @throws SegueDatabaseException * - if there is an error during the database update. * @throws MissingRequiredFieldException * - If the change will mean that the user will be unable to login again. * @throws AuthenticationProviderMappingException * - if we are unable to locate the authentication provider specified. */ public void unlinkUserFromProvider(final RegisteredUserDTO user, final String providerString) throws SegueDatabaseException, MissingRequiredFieldException, AuthenticationProviderMappingException { RegisteredUser userDO = this.findUserById(user.getId()); this.userAuthenticationManager.unlinkUserAndProvider(userDO, providerString); } /** * CheckUserRole matches a list of valid roles. * * @param request * - http request so that we can get current users details. * @param validRoles * - a Collection of roles that we would want the user to match. * @return true if the user is a member of one of the roles in our valid roles list. False if not. * @throws NoUserLoggedInException * - if there is no registered user logged in. */ public final boolean checkUserRole(final HttpServletRequest request, final Collection<Role> validRoles) throws NoUserLoggedInException { RegisteredUser user = this.getCurrentRegisteredUserDO(request); if (null == user) { throw new NoUserLoggedInException(); } for (Role roleToMatch : validRoles) { if (user.getRole() != null && user.getRole().equals(roleToMatch)) { return true; } } return false; } /** * Determine if there is a user logged in with a valid session. * * @param request * - to retrieve session information from * @return True if the user is logged in and the session is valid, false if not. */ public final boolean isRegisteredUserLoggedIn(final HttpServletRequest request) { try { return this.getCurrentRegisteredUser(request) != null; } catch (NoUserLoggedInException e) { return false; } } /** * Get the details of the currently logged in registered user. * * This method will validate the session and will throw a NoUserLoggedInException if invalid. * * @param request * - to retrieve session information from * @return Returns the current UserDTO if we can get it or null if user is not currently logged in * @throws NoUserLoggedInException * - When the session has expired or there is no user currently logged in. */ public final RegisteredUserDTO getCurrentRegisteredUser(final HttpServletRequest request) throws NoUserLoggedInException { Validate.notNull(request); RegisteredUser user = this.getCurrentRegisteredUserDO(request); if (null == user) { throw new NoUserLoggedInException(); } try { updateLastSeen(user); } catch (SegueDatabaseException e) { log.error(String.format("Unable to update user (%s) last seen date.", user.getId())); } return this.convertUserDOToUserDTO(user); } /** * Find a list of users based on some user prototype. * * @param prototype * - partially completed user object to base search on * @return list of registered user dtos. * @throws SegueDatabaseException * - if there is a database error. */ public final List<RegisteredUserDTO> findUsers(final RegisteredUserDTO prototype) throws SegueDatabaseException { List<RegisteredUser> registeredUsersDOs = this.database .findUsers(this.dtoMapper.map(prototype, RegisteredUser.class)); return this.convertUserDOToUserDTOList(registeredUsersDOs); } /** * Find a list of users based on a List of user ids. * * @param userIds * - partially completed user object to base search on * @return list of registered user dtos. * @throws SegueDatabaseException * - if there is a database error. */ public final List<RegisteredUserDTO> findUsers(final List<Long> userIds) throws SegueDatabaseException { Validate.notNull(userIds); if (userIds.isEmpty()) { return Lists.newArrayList(); } List<RegisteredUser> registeredUsersDOs = this.database.findUsers(userIds); return this.convertUserDOToUserDTOList(registeredUsersDOs); } /** * This function can be used to find user information about a user when given an id. * * @param id * - the id of the user to search for. * @return the userDTO * @throws NoUserException * - If we cannot find a valid user with the email address provided. * @throws SegueDatabaseException * - If there is another database error */ public final RegisteredUserDTO getUserDTOById(final Long id) throws NoUserException, SegueDatabaseException { return this.convertUserDOToUserDTO(this.findUserById(id)); } /** * This function can be used to find user information about a user when given an email. * * @param email * - the e-mail address of the user to search for * @return the userDTO * @throws NoUserException * - If we cannot find a valid user with the email address provided. * @throws SegueDatabaseException * - If there is another database error */ public final RegisteredUserDTO getUserDTOByEmail(final String email) throws NoUserException, SegueDatabaseException { RegisteredUser findUserByEmail = this.findUserByEmail(email); if (null == findUserByEmail) { throw new NoUserException(); } return this.convertUserDOToUserDTO(findUserByEmail); } /** * This method will return either an AnonymousUserDTO or a RegisteredUserDTO * * If the user is currently logged in you will get a RegisteredUserDTO otherwise you will get an AnonymousUserDTO * containing a sessionIdentifier and any questionAttempts made by the anonymous user. * * @param request * - containing session information. * * @return AbstractSegueUserDTO - Either a RegisteredUser or an AnonymousUser */ public AbstractSegueUserDTO getCurrentUser(final HttpServletRequest request) { try { return this.getCurrentRegisteredUser(request); } catch (NoUserLoggedInException e) { return this.getAnonymousUserDTO(request); } } /** * Destroy a session attached to the request. * * @param request * containing the tomcat session to destroy * @param response * to destroy the segue cookie. */ public void logUserOut(final HttpServletRequest request, final HttpServletResponse response) { Validate.notNull(request); this.userAuthenticationManager.destroyUserSession(request, response); } /** * Method to create a user object in our database. * * @param request * to enable access to anonymous user information. * @param response * to store the session in our own segue cookie. * @param user * - the user DO to use for updates - must not contain a user id. * @throws InvalidPasswordException * - the password provided does not meet our requirements. * @throws MissingRequiredFieldException * - A required field is missing for the user object so cannot be saved. * @return the user object as was saved. * @throws SegueDatabaseException * - If there is an internal database error. * @throws AuthenticationProviderMappingException * - if there is a problem locating the authentication provider. This only applies for changing a * password. * @throws EmailMustBeVerifiedException * - if a user attempts to sign up with an email that must be verified before it can be used * (i.e. an @isaacphysics.org or @isaacchemistry.org address). */ public RegisteredUserDTO createUserObjectAndSession(final HttpServletRequest request, final HttpServletResponse response, final RegisteredUser user) throws InvalidPasswordException, MissingRequiredFieldException, SegueDatabaseException, AuthenticationProviderMappingException, EmailMustBeVerifiedException { Validate.isTrue(user.getId() == null, "When creating a new user the user id must not be set."); if (this.findUserByEmail(user.getEmail()) != null) { throw new DuplicateAccountException("An account with that e-mail address already exists."); } // Ensure nobody registers with Isaac email addresses. Users can change emails by verifying them however. if (user.getEmail().matches(".*@isaac(physics|chemistry|biology|science)\\.org")) { log.warn("User attempted to register with Isaac email address '" + user.getEmail() + "'!"); throw new EmailMustBeVerifiedException("You cannot register with an Isaac email address."); } RegisteredUser userToSave = null; MapperFacade mapper = this.dtoMapper; // We want to map to DTO first to make sure that the user cannot // change fields that aren't exposed to them RegisteredUserDTO userDtoForNewUser = mapper.map(user, RegisteredUserDTO.class); // This is a new registration userToSave = mapper.map(userDtoForNewUser, RegisteredUser.class); // Set defaults userToSave.setRole(Role.STUDENT); userToSave.setEmailVerificationStatus(EmailVerificationStatus.NOT_VERIFIED); userToSave.setRegistrationDate(new Date()); userToSave.setLastUpdated(new Date()); this.userAuthenticationManager.checkForSeguePasswordChange(user, userToSave); // Before save we should validate the user for mandatory fields. if (!this.isUserValid(userToSave)) { throw new MissingRequiredFieldException("The user provided is missing a mandatory field"); } else if (!this.database.hasALinkedAccount(userToSave) && userToSave.getPassword() == null) { // a user must have a way of logging on. throw new MissingRequiredFieldException("This modification would mean that the user" + " no longer has a way of authenticating. Reverting change."); } IPasswordAuthenticator authenticator = (IPasswordAuthenticator) this.registeredAuthProviders .get(AuthenticationProvider.SEGUE); try { authenticator.createEmailVerificationTokenForUser(userToSave, userToSave.getEmail()); } catch (NoSuchAlgorithmException e1) { log.error("Creation of email verification token failed: " + e1.getMessage()); } catch (InvalidKeySpecException e1) { log.error("Creation of email verification token failed: " + e1.getMessage()); } // save the user to get the userId RegisteredUser userToReturn = this.database.createOrUpdateUser(userToSave); // send an email confirmation and set up verification try { RegisteredUserDTO userToReturnDTO = this.getUserDTOById(userToReturn.getId()); ImmutableMap<String, Object> emailTokens = ImmutableMap.of("verificationURL", generateEmailVerificationURL(userToReturnDTO, userToReturn.getEmailVerificationToken())); emailManager.sendTemplatedEmailToUser(userToReturnDTO, emailManager.getEmailTemplateDTO("email-template-registration-confirmation"), emailTokens, EmailType.SYSTEM); } catch (ContentManagerException e) { log.error("Registration email could not be sent due to content issue: " + e.getMessage()); } catch (NoUserException e) { log.error("Registration email could not be sent due to content issue: " + e.getMessage()); } // save the user again with updated token userToReturn = this.database.createOrUpdateUser(userToReturn); logManager.logInternalEvent(this.convertUserDOToUserDTO(userToReturn), Constants.USER_REGISTRATION, ImmutableMap.builder().put("provider", AuthenticationProvider.SEGUE.name()).build()); // return it to the caller. return this.logUserIn(request, response, userToReturn); } /** * Method to update a user object in our database. * * @param updatedUser * - the user to update - must contain a user id * @throws InvalidPasswordException * - the password provided does not meet our requirements. * @throws MissingRequiredFieldException * - A required field is missing for the user object so cannot be saved. * @return the user object as was saved. * @throws SegueDatabaseException * - If there is an internal database error. * @throws AuthenticationProviderMappingException * - if there is a problem locating the authentication provider. This only applies for changing a * password. */ public RegisteredUserDTO updateUserObject(final RegisteredUser updatedUser) throws InvalidPasswordException, MissingRequiredFieldException, SegueDatabaseException, AuthenticationProviderMappingException { Validate.notNull(updatedUser.getId()); MapperFacade mapper = this.dtoMapper; // We want to map to DTO first to make sure that the user cannot // change fields that aren't exposed to them RegisteredUserDTO userDTOContainingUpdates = mapper.map(updatedUser, RegisteredUserDTO.class); if (updatedUser.getId() == null) { throw new IllegalArgumentException( "The user object specified has an id. Users cannot be updated without a specific id already set."); } // This is an update operation. final RegisteredUser existingUser = this.findUserById(updatedUser.getId()); // userToSave = existingUser; // Check that the user isn't trying to take an existing users e-mail. if (this.findUserByEmail(updatedUser.getEmail()) != null && !existingUser.getEmail().equals(updatedUser.getEmail())) { throw new DuplicateAccountException("An account with that e-mail address already exists."); } // Send a new verification email if the user has changed their email if (!existingUser.getEmail().equals(updatedUser.getEmail())) { IPasswordAuthenticator authenticator = (IPasswordAuthenticator) this.registeredAuthProviders .get(AuthenticationProvider.SEGUE); try { authenticator.createEmailVerificationTokenForUser(existingUser, updatedUser.getEmail()); } catch (NoSuchAlgorithmException | InvalidKeySpecException e1) { log.error("Creation of email verification token failed: " + e1.getMessage()); } log.info(String.format( "Sending email for email address change for user (%s)" + " from email (%s) to email (%s)", updatedUser.getId(), existingUser.getEmail(), updatedUser.getEmail())); try { RegisteredUserDTO existingUserDTO = this.getUserDTOById(existingUser.getId()); emailManager.sendTemplatedEmailToUser(existingUserDTO, emailManager.getEmailTemplateDTO("email-verification-change"), ImmutableMap.of("requestedemail", updatedUser.getEmail()), EmailType.SYSTEM); } catch (ContentManagerException | NoUserException e) { log.debug("ContentManagerException during sendEmailVerificationChange " + e.getMessage()); } } // Send a welcome email if the user has become a teacher try { RegisteredUserDTO existingUserDTO = this.getUserDTOById(existingUser.getId()); if (updatedUser.getRole() != existingUser.getRole()) { //TODO: refactor and just use updateUserRole method for the below switch (updatedUser.getRole()) { case TEACHER: emailManager.sendTemplatedEmailToUser(existingUserDTO, emailManager.getEmailTemplateDTO("email-template-teacher-welcome"), ImmutableMap.of("oldrole", existingUserDTO.getRole().toString(), "newrole", updatedUser.getRole().toString()), EmailType.SYSTEM); break; default: emailManager.sendTemplatedEmailToUser(existingUserDTO, emailManager.getEmailTemplateDTO("email-template-default-role-change"), ImmutableMap.of("oldrole", existingUserDTO.getRole().toString(), "newrole", updatedUser.getRole().toString()), EmailType.SYSTEM); break; } } } catch (ContentManagerException | NoUserException e) { log.debug("ContentManagerException during sendTeacherWelcome " + e.getMessage()); } MapperFacade mergeMapper = new DefaultMapperFactory.Builder().mapNulls(false).build().getMapperFacade(); RegisteredUser userToSave = new RegisteredUser(); mergeMapper.map(existingUser, userToSave); mergeMapper.map(userDTOContainingUpdates, userToSave); userToSave.setRegistrationDate(existingUser.getRegistrationDate()); userToSave.setLastUpdated(new Date()); if (updatedUser.getSchoolId() == null && existingUser.getSchoolId() != null) { userToSave.setSchoolId(null); } // Correctly remove school_other when it is set to be the empty string: if (updatedUser.getSchoolOther() == null || updatedUser.getSchoolOther().isEmpty()) { userToSave.setSchoolOther(null); } this.userAuthenticationManager.checkForSeguePasswordChange(updatedUser, userToSave); // Before save we should validate the user for mandatory fields. if (!this.isUserValid(userToSave)) { throw new MissingRequiredFieldException("The user provided is missing a mandatory field"); } else if (!this.database.hasALinkedAccount(userToSave) && userToSave.getPassword() == null) { // a user must have a way of logging on. throw new MissingRequiredFieldException("This modification would mean that the user" + " no longer has a way of authenticating. Failing change."); } // Make sure the email address is preserved (can't be changed until new email is verified) if (!userToSave.getEmail().equals(existingUser.getEmail())) { try { RegisteredUserDTO userToSaveDTO = mapper.map(userToSave, RegisteredUserDTO.class); Map<String, Object> emailTokens = ImmutableMap.of("verificationURL", this.generateEmailVerificationURL(userToSaveDTO, userToSave.getEmailVerificationToken())); emailManager.sendTemplatedEmailToUser(userToSaveDTO, emailManager.getEmailTemplateDTO("email-template-email-verification"), emailTokens, EmailType.SYSTEM); } catch (ContentManagerException e) { log.debug("ContentManagerException during sendEmailVerification " + e.getMessage()); } userToSave.setEmail(existingUser.getEmail()); } // save the user RegisteredUser userToReturn = this.database.createOrUpdateUser(userToSave); // return it to the caller return this.convertUserDOToUserDTO(userToReturn); } /** * @param id * - the user id * @param requestedRole * - the new role * @throws SegueDatabaseException * - an exception when accessing the database */ public void updateUserRole(final Long id, final Role requestedRole) throws SegueDatabaseException { Validate.notNull(requestedRole); RegisteredUser userToSave = this.findUserById(id); // Send welcome email if user has become teacher, otherwise, role change notification try { RegisteredUserDTO existingUserDTO = this.getUserDTOById(id); if (userToSave.getRole() != requestedRole) { switch (requestedRole) { case TEACHER: emailManager.sendTemplatedEmailToUser(existingUserDTO, emailManager.getEmailTemplateDTO("email-template-teacher-welcome"), ImmutableMap.of("oldrole", existingUserDTO.getRole().toString(), "newrole", requestedRole.toString()), EmailType.SYSTEM); break; default: emailManager.sendTemplatedEmailToUser(existingUserDTO, emailManager.getEmailTemplateDTO("email-template-default-role-change"), ImmutableMap.of("oldrole", existingUserDTO.getRole().toString(), "newrole", requestedRole.toString()), EmailType.SYSTEM); break; } } } catch (ContentManagerException | NoUserException e) { log.debug("ContentManagerException during sendTeacherWelcome " + e.getMessage()); } userToSave.setRole(requestedRole); this.database.createOrUpdateUser(userToSave); } /** * @param email * - the user email * @param requestedEmailVerificationStatus * - the new email verification status * @throws SegueDatabaseException * - an exception when accessing the database */ public void updateUserEmailVerificationStatus(final String email, final EmailVerificationStatus requestedEmailVerificationStatus) throws SegueDatabaseException { Validate.notNull(requestedEmailVerificationStatus); RegisteredUser userToSave = this.findUserByEmail(email); if (null == userToSave) { log.warn(String.format( "Could not update email verification status of email address (%s) - does not exist", email)); return; } userToSave.setEmailVerificationStatus(requestedEmailVerificationStatus); this.database.createOrUpdateUser(userToSave); } /** * This method facilitates the removal of personal user data from Segue. * * @param userToDelete * - the user to delete. * @throws SegueDatabaseException * - if a general database error has occurred. * @throws NoUserException * - if we cannot find the user account specified */ public void deleteUserAccount(final RegisteredUserDTO userToDelete) throws NoUserException, SegueDatabaseException { Validate.notNull(userToDelete); // check the user exists RegisteredUser userDOById = this.findUserById(userToDelete.getId()); // delete the user. this.database.deleteUserAccount(userDOById); } /** * This method will use an email address to check a local user exists and if so, will send an email with a unique * token to allow a password reset. This method does not indicate whether or not the email actually existed. * * @param userObject * - A user object containing the email address of the user to reset the password for. * @throws NoSuchAlgorithmException * - if the configured algorithm is not valid. * @throws InvalidKeySpecException * - if the preconfigured key spec is invalid. * @throws CommunicationException * - if a fault occurred whilst sending the communique * @throws SegueDatabaseException * - If there is an internal database error. */ public final void resetPasswordRequest(final RegisteredUserDTO userObject) throws InvalidKeySpecException, NoSuchAlgorithmException, CommunicationException, SegueDatabaseException, NoUserException { RegisteredUser user = this.findUserByEmail(userObject.getEmail()); if (null == user) { throw new NoUserException(); } RegisteredUserDTO userDTO = this.convertUserDOToUserDTO(user); this.userAuthenticationManager.resetPasswordRequest(user, userDTO); } /** * This method will use an email address to check a local user exists and if so, will send an email with a unique * token to allow a password reset. This method does not indicate whether or not the email actually existed. * * @param request * - so we can look up the registered user object. * @param email * - The email the user wants to verify. * @throws NoSuchAlgorithmException * - if the configured algorithm is not valid. * @throws InvalidKeySpecException * - if the preconfigured key spec is invalid. * @throws CommunicationException * - if a fault occurred whilst sending the communique * @throws SegueDatabaseException * - If there is an internal database error. */ public final void emailVerificationRequest(final HttpServletRequest request, final String email) throws InvalidKeySpecException, NoSuchAlgorithmException, CommunicationException, SegueDatabaseException { RegisteredUser user = this.findUserByEmail(email); if (null == user) { try { RegisteredUserDTO userDTO = getCurrentRegisteredUser(request); user = this.findUserById(userDTO.getId()); } catch (NoUserLoggedInException e) { log.error(String.format("Verification requested for email:%s where email does not exist " + "and user not logged in!", email)); } } if (user == null) { // Email address does not exist in the DB // Fail silently return; } // TODO: Email verification stuff does not belong in the password authenticator... It should be moved. // Generate token IPasswordAuthenticator authenticator = (IPasswordAuthenticator) this.registeredAuthProviders .get(AuthenticationProvider.SEGUE); user = authenticator.createEmailVerificationTokenForUser(user, email); // Save user object this.database.createOrUpdateUser(user); log.info(String.format("Sending password reset message to %s", user.getEmail())); try { RegisteredUserDTO userDTO = this.getUserDTOById(user.getId()); Map<String, Object> emailTokens = ImmutableMap.of("verificationURL", this.generateEmailVerificationURL(userDTO, user.getEmailVerificationToken())); emailManager.sendTemplatedEmailToUser(userDTO, emailManager.getEmailTemplateDTO("email-template-email-verification"), emailTokens, EmailType.SYSTEM); } catch (ContentManagerException e) { log.debug("ContentManagerException " + e.getMessage()); } catch (NoUserException e) { log.debug("NoUserException " + e.getMessage()); } } /** * This method will test if the specified token is a valid password reset token. * * * @param token * - The token to test * @return true if the reset token is valid * @throws SegueDatabaseException * - If there is an internal database error. */ public final boolean validatePasswordResetToken(final String token) throws SegueDatabaseException { // Set user's password IPasswordAuthenticator authenticator = (IPasswordAuthenticator) this.registeredAuthProviders .get(AuthenticationProvider.SEGUE); return authenticator.isValidResetToken(this.findUserByResetToken(token)); } /** * processEmailVerification. * @param userid * - the user id * * @param email * - the email address - may be new or the same * * @param token * - token used to verify email address * * @return - whether the token is valid or not * @throws SegueDatabaseException * - exception if token cannot be validated * @throws InvalidTokenException - if something is wrong with the token provided * @throws NoUserException - if the user does not exist. */ public RegisteredUserDTO processEmailVerification(final Long userid, final String email, final String token) throws SegueDatabaseException, InvalidTokenException, NoUserException { IPasswordAuthenticator authenticator = (IPasswordAuthenticator) this.registeredAuthProviders .get(AuthenticationProvider.SEGUE); RegisteredUser user = this.findUserById(userid); if (null == user) { log.warn(String.format("Recieved an invalid email token request for (%s)", email)); throw new NoUserException(); } if (!userid.equals(user.getId())) { log.warn(String.format("Recieved an invalid email token request for (%s)" + " - provided bad userid", email)); throw new InvalidTokenException(); } EmailVerificationStatus evStatus = user.getEmailVerificationStatus(); if (evStatus != null && evStatus == EmailVerificationStatus.VERIFIED && user.getEmail().equals(email)) { log.warn(String.format("Recieved a duplicate email verification request for (%s) - already verified", email)); return this.convertUserDOToUserDTO(user); } if (authenticator.isValidEmailVerificationToken(user, email, token)) { user.setEmailVerificationStatus(EmailVerificationStatus.VERIFIED); user.setEmailVerificationToken(null); // Update the email address if different if (!user.getEmail().equals(email)) { user.setEmail(email); } // Save user RegisteredUser createOrUpdateUser = this.database.createOrUpdateUser(user); log.info(String.format("Email verification for user (%s) has completed successfully.", createOrUpdateUser.getId())); return this.convertUserDOToUserDTO(createOrUpdateUser); } else { log.warn(String.format("Recieved an invalid email verification token for (%s) - invalid token", email)); throw new InvalidTokenException(); } } /** * This method will use a unique password reset token to set a new password. * * @param token * - the password reset token * @param userObject * - the supplied user DO * @return the user which has had the password reset. * @throws InvalidTokenException * - If the token provided is invalid. * @throws InvalidPasswordException * - If the password provided is invalid. * @throws SegueDatabaseException * - If there is an internal database error. */ public RegisteredUserDTO resetPassword(final String token, final RegisteredUser userObject) throws InvalidTokenException, InvalidPasswordException, SegueDatabaseException { return this.convertUserDOToUserDTO(this.userAuthenticationManager.resetPassword(token, userObject)); } /** * Helper method to convert a user object into a cutdown userSummary DTO. * * @param userToConvert * - full user object. * @return a summarised object with minimal personal information */ public UserSummaryDTO convertToUserSummaryObject(final RegisteredUserDTO userToConvert) { return this.dtoMapper.map(userToConvert, UserSummaryDTO.class); } /** * Helper method to convert a user object into a cutdown detailedUserSummary DTO. * * @param userToConvert * - full user object. * @return a summarised object with reduced personal information */ public DetailedUserSummaryDTO convertToDetailedUserSummaryObject(final RegisteredUserDTO userToConvert) { return this.dtoMapper.map(userToConvert, DetailedUserSummaryDTO.class); } /** * Helper method to convert user objects into cutdown userSummary DTOs. * * @param userListToConvert * - full user objects. * @return a list of summarised objects with minimal personal information */ public List<UserSummaryDTO> convertToUserSummaryObjectList(final List<RegisteredUserDTO> userListToConvert) { Validate.notNull(userListToConvert); List<UserSummaryDTO> resultList = Lists.newArrayList(); for (RegisteredUserDTO user : userListToConvert) { resultList.add(this.convertToUserSummaryObject(user)); } return resultList; } /** * Helper method to convert user objects into cutdown DetailedUserSummary DTOs. * * @param userListToConvert * - full user objects. * @return a list of summarised objects with reduced personal information */ public List<DetailedUserSummaryDTO> convertToDetailedUserSummaryObjectList( final List<RegisteredUserDTO> userListToConvert) { Validate.notNull(userListToConvert); List<DetailedUserSummaryDTO> resultList = Lists.newArrayList(); for (RegisteredUserDTO user : userListToConvert) { resultList.add(this.convertToDetailedUserSummaryObject(user)); } return resultList; } /** * Logs the user in and creates the signed sessions. * * @param request * - for the session to be attached * @param response * - for the session to be attached. * @param user * - the user who is being logged in. * @return the DTO version of the user. */ private RegisteredUserDTO logUserIn(final HttpServletRequest request, final HttpServletResponse response, final RegisteredUser user) { AnonymousUser anonymousUser = this.getAnonymousUserDO(request); // now we want to clean up any data generated by the user while they weren't logged in. mergeAnonymousUserWithRegisteredUser(anonymousUser, user); return this .convertUserDOToUserDTO(this.userAuthenticationManager.createUserSession(request, response, user)); } /** * Method to migrate anonymously generated data to a persisted account. * * @param anonymousUser * to look up. * @param user * to migrate to. */ private void mergeAnonymousUserWithRegisteredUser(final AnonymousUser anonymousUser, final RegisteredUser user) { if (anonymousUser != null) { // merge any anonymous information collected with this user. try { final RegisteredUserDTO userDTO = this.convertUserDOToUserDTO(user); this.questionAttemptDb.mergeAnonymousQuestionAttemptsIntoRegisteredUser( this.dtoMapper.map(anonymousUser, AnonymousUserDTO.class), userDTO); // may as well spawn a new thread to do the log migration stuff asynchronously // work now. Thread logMigrationJob = new Thread() { @Override public void run() { // run this asynchronously as there is no need to block and it is quite slow. logManager.transferLogEventsToRegisteredUser(anonymousUser.getSessionId(), user.getId().toString()); logManager.logInternalEvent(userDTO, MERGE_USER, ImmutableMap.of("oldAnonymousUserId", anonymousUser.getSessionId())); // delete the session attribute as merge has completed. temporaryUserCache.invalidate(anonymousUser.getSessionId()); } }; logMigrationJob.start(); } catch (SegueDatabaseException e) { log.error("Unable to merge anonymously collected data with stored user object.", e); } } } /** * Library method that allows the api to locate a user object from the database based on a given unique id. * * @param userId * - to search for. * @return user or null if we cannot find it. * @throws SegueDatabaseException * - If there is an internal database error. */ private RegisteredUser findUserById(final Long userId) throws SegueDatabaseException { if (null == userId) { return null; } return this.database.getById(userId); } /** * Library method that allows the api to locate a user object from the database based on a given unique email * address. * * @param email * - to search for. * @return user or null if we cannot find it. * @throws SegueDatabaseException * - If there is an internal database error. */ private RegisteredUser findUserByEmail(final String email) throws SegueDatabaseException { if (null == email) { return null; } return this.database.getByEmail(email); } /** * Library method that allows the api to locate a user object from the database based on a given unique password * reset token. * * @param token * - to search for. * @return user or null if we cannot find it. * @throws SegueDatabaseException * - If there is an internal database error. */ private RegisteredUser findUserByResetToken(final String token) throws SegueDatabaseException { if (null == token) { return null; } return this.database.getByResetToken(token); } /** * This method should use the provider specific reference to either register a new user or retrieve an existing * user. * * @param federatedAuthenticator * the federatedAuthenticator we are using for authentication * @param userFromProvider * - the user object returned by the auth provider. * @return a Segue UserDO that exists in the segue database. * @throws AuthenticatorSecurityException * - error with authenticator. * @throws NoUserException * - If we are unable to locate the user id based on the lookup reference provided. * @throws IOException * - if there is an io error. * @throws SegueDatabaseException * - If there is an internal database error. */ private RegisteredUser registerUserWithFederatedProvider(final AuthenticationProvider federatedAuthenticator, final UserFromAuthProvider userFromProvider) throws AuthenticatorSecurityException, NoUserException, IOException, SegueDatabaseException { log.debug(String.format("New registration (%s) as user does not already exist.", federatedAuthenticator)); if (null == userFromProvider) { log.warn("Unable to create user for the provider " + federatedAuthenticator); throw new NoUserException(); } RegisteredUser newLocalUser = this.dtoMapper.map(userFromProvider, RegisteredUser.class); newLocalUser.setRegistrationDate(new Date()); // register user RegisteredUser newlyRegisteredUser = database.registerNewUserWithProvider(newLocalUser, federatedAuthenticator, userFromProvider.getProviderUserId()); RegisteredUser localUserInformation = this.database.getById(newlyRegisteredUser.getId()); if (null == localUserInformation) { // we just put it in so something has gone very wrong. log.error("Failed to retreive user even though we " + "just put it in the database."); throw new NoUserException(); } // since the federated providers didn't always provide email addresses - we have to check and update accordingly. if (!localUserInformation.getEmail().contains("@") && !EmailVerificationStatus.DELIVERY_FAILED .equals(localUserInformation.getEmailVerificationStatus())) { this.updateUserEmailVerificationStatus(localUserInformation.getEmail(), EmailVerificationStatus.DELIVERY_FAILED); } logManager.logInternalEvent(this.convertUserDOToUserDTO(localUserInformation), Constants.USER_REGISTRATION, ImmutableMap.builder().put("provider", federatedAuthenticator.name()).build()); return localUserInformation; } /** * IsUserValid This function will check that the user object is valid. * * @param userToValidate * - the user to validate. * @return true if it meets the internal storage requirements, false if not. */ private boolean isUserValid(final RegisteredUser userToValidate) { boolean isValid = true; if (userToValidate.getEmail() == null || userToValidate.getEmail().isEmpty() || !userToValidate.getEmail().contains("@")) { isValid = false; } return isValid; } /** * Converts the sensitive UserDO into a limited DTO. * * @param user * - DO * @return user - DTO */ private RegisteredUserDTO convertUserDOToUserDTO(final RegisteredUser user) { if (null == user) { return null; } RegisteredUserDTO userDTO = this.dtoMapper.map(user, RegisteredUserDTO.class); // Augment with linked account information try { userDTO.setLinkedAccounts(this.database.getAuthenticationProvidersByUser(user)); } catch (SegueDatabaseException e) { log.error("Unable to set linked accounts for user due to a database error."); } if (user.getPassword() != null && !user.getPassword().isEmpty()) { userDTO.setHasSegueAccount(true); } else { userDTO.setHasSegueAccount(false); } return userDTO; } /** * Converts a list of userDOs into a List of userDTOs. * * @param listToConvert * - list of DOs to convert * @return the list of user dtos. */ private List<RegisteredUserDTO> convertUserDOToUserDTOList(final List<RegisteredUser> listToConvert) { List<RegisteredUserDTO> result = Lists.newArrayList(); for (RegisteredUser user : listToConvert) { result.add(this.convertUserDOToUserDTO(user)); } return result; } /** * Get the RegisteredUserDO of the currently logged in user. This is for internal use only. * * This method will validate the session as well returning null if it is invalid. * * @param request * - to retrieve session information from * @return Returns the current UserDTO if we can get it or null if user is not currently logged in / there is an * invalid session */ private RegisteredUser getCurrentRegisteredUserDO(final HttpServletRequest request) { return this.userAuthenticationManager.getUserFromSession(request); } /** * Retrieves anonymous user information if it is available. * * @param request * - request containing session information. * @return An anonymous user containing any anonymous question attempts (which could be none) */ private AnonymousUserDTO getAnonymousUserDTO(final HttpServletRequest request) { return this.dtoMapper.map(this.getAnonymousUserDO(request), AnonymousUserDTO.class); } /** * Retrieves anonymous user information if it is available. * * @param request * - request containing session information. * @return An anonymous user containing any anonymous question attempts (which could be none) */ private AnonymousUser getAnonymousUserDO(final HttpServletRequest request) { AnonymousUser user; // no session exists so create one. if (request.getSession().getAttribute(ANONYMOUS_USER) == null) { user = new AnonymousUser(request.getSession().getId()); user.setDateCreated(new Date()); // add the user reference to the session request.getSession().setAttribute(ANONYMOUS_USER, user.getSessionId()); this.temporaryUserCache.put(user.getSessionId(), user); } else { // reuse existing one if (request.getSession().getAttribute(ANONYMOUS_USER) instanceof String) { String userId = (String) request.getSession().getAttribute(ANONYMOUS_USER); user = this.temporaryUserCache.getIfPresent(userId); if (null == user) { // the session must have expired. Create a new user and run this method again. // this probably won't happen often as the session expiry and the cache should be timed correctly. request.getSession().removeAttribute(ANONYMOUS_USER); log.warn("Anonymous user session expired so creating a" + " new one - this should not happen often if cache settings are correct."); return this.getAnonymousUserDO(request); } } else { // this means that someone has put the wrong type in to the session variable. throw new ClassCastException("Unable to get AnonymousUser from session."); } } return user; } /** * Update the users' last seen field. * * @param user * of interest * @throws SegueDatabaseException * - if an error occurs with the update. */ private void updateLastSeen(final RegisteredUser user) throws SegueDatabaseException { if (user.getLastSeen() == null) { this.database.updateUserLastSeen(user); } else { // work out if we should update the user record again... long timeDiff = Math.abs(new Date().getTime() - user.getLastSeen().getTime()); long minutesElapsed = TimeUnit.MILLISECONDS.toMinutes(timeDiff); if (minutesElapsed > LAST_SEEN_UPDATE_FREQUENCY_MINUTES) { this.database.updateUserLastSeen(user); } } } /** * @param userDTO * @param emailVerificationToken * @return */ private String generateEmailVerificationURL(final RegisteredUserDTO userDTO, final String emailVerificationToken) { final int TRUNCATED_TOKEN_LENGTH = 5; List<NameValuePair> urlParamPairs = Lists.newArrayList(); urlParamPairs.add(new BasicNameValuePair("userid", userDTO.getId().toString())); urlParamPairs.add(new BasicNameValuePair("email", userDTO.getEmail().toString())); urlParamPairs .add(new BasicNameValuePair("token", emailVerificationToken.substring(0, TRUNCATED_TOKEN_LENGTH))); String urlParams = URLEncodedUtils.format(urlParamPairs, "UTF-8"); return String.format("https://%s/verifyemail?%s", properties.getProperty(HOST_NAME), urlParams); } }