Get the Code: https://github.com/iainporter/rest-java
Password reset is similar to email registration covered in Part Three. The essential parts involve generating a short-lived unique token, emailing it to the user and handling the return of the token.
- User clicks on lost password link
- User enters their email address and submits
- The server generates a short-lived token and sends email to user address with the Base64 encoded token in an embedded link
- User clicks on link (or pastes it into browser window)
- User enters new password which is submitted along with the token to the server
- Server validates the token and password and matches it up to the User
- Password is hashed and saved to User account
The Verification Token
The main properties of a VerificationToken are:
- token - a UUID that is used to identify the token. It is Base64 encoded before being sent
- expiryDate - time to live for the Token. Configured in app.properties
- tokenType - enum (lostPassword, emailVerification, emailRegistration)
- verified - has this token been verified
Verification Token Service
The method for generating and sending the token
/** * generate token if user found otherwise do nothing * * @param emailAddress * @return a token or null if user not found */ @Transactional public VerificationToken sendLostPasswordToken(String emailAddress) { Assert.notNull(emailAddress); VerificationToken token = null; User user = userRepository.findByEmailAddress(emailAddress); if (user != null) { token = user.getActiveLostPasswordToken(); if (token == null) { token = new VerificationToken(user, VerificationToken.VerificationTokenType.lostPassword, config.getLostPasswordTokenExpiryTimeInMinutes()); user.addVerificationToken(token); userRepository.save(user); } emailServicesGateway.sendVerificationToken(new EmailServiceTokenModel(user, token, getConfig().getHostNameUrl())); } return token; }
First, find the user by Email Address (line 11). If there is no account matching the address then we don't want to throw an exception but just ignore processing. We could wire in some logic to send an email telling the user that they attempted to change their password but their account does not exist. The main reason for the obfuscation is to prevent malicious trolling of the application to determine if a particular email account is registered.
Once a new token is generated it is passed off for asynchronous processing to the email services gateway.
Email Services Gateway
The service gateway uses Spring Integration to route email tasks. The task is first queued to guarantee delivery and marks the thread boundary of the calling process.
<int:gateway id="emailServicesGateway" service-interface="com.porterhead.rest.gateway.EmailServicesGateway" default-reply-timeout="3000"> <int:method name="sendVerificationToken" request-channel="emailVerificationRouterChannel" request-timeout="3000"/> </int:gateway>
A router polls the queue and routes the email task to the appropriate service.
<int:channel id="emailVerificationRouterChannel"> <int:queue capacity="1000" message-store="emailVerificationMessageStore"/> </int:channel> <int:router id="emailVerificationRouter" input-channel="emailVerificationRouterChannel" expression="payload.getTokenType()"> <int:poller fixed-rate="2000"> <int:transactional/> </int:poller> <int:mapping value="emailVerification" channel="emailVerificationTokenSendChannel"/> <int:mapping value="emailRegistration" channel="emailRegistrationTokenSendChannel"/> <int:mapping value="lostPassword" channel="emailLostPasswordTokenSendChannel"/> </int:router> <int:channel id="emailLostPasswordTokenSendChannel"/> <int:service-activator id="emailLostPasswordSenderService" input-channel="emailLostPasswordTokenSendChannel" output-channel="nullChannel" ref="mailSenderService" method="sendLostPasswordEmail"> </int:service-activator>
Mail Sender Service
The service loads a velocity template and merges it with the email token model
public EmailServiceTokenModel sendLostPasswordEmail(final EmailServiceTokenModel emailServiceTokenModel) { Map<String, String> resources = new HashMap%lt;String, String>(); return sendVerificationEmail(emailServiceTokenModel, config.getLostPasswordSubjectText(), "META-INF/velocity/LostPasswordEmail.vm", resources); }
When the template has been merged the email is sent using JavaMailSender
private EmailServiceTokenModel sendVerificationEmail(final EmailServiceTokenModel emailVerificationModel, final String emailSubject, final String velocityModel, final Map<String, String> resources) { MimeMessagePreparator preparator = new MimeMessagePreparator() { public void prepare(MimeMessage mimeMessage) throws Exception { MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, MimeMessageHelper.MULTIPART_MODE_RELATED, "UTF-8"); messageHelper.setTo(emailVerificationModel.getEmailAddress()); messageHelper.setFrom(config.getEmailFromAddress()); messageHelper.setReplyTo(config.getEmailReplyToAddress()); messageHelper.setSubject(emailSubject); Map model = new HashMap(); model.put("model", emailVerificationModel); String text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, velocityModel, model); messageHelper.setText(new String(text.getBytes(), "UTF-8"), true); for(String resourceIdentifier: resources.keySet()) { addInlineResource(messageHelper, resources.get(resourceIdentifier), resourceIdentifier); } } }; LOG.debug("Sending {} token to : {}",emailVerificationModel.getTokenType().toString(), emailVerificationModel.getEmailAddress()); this.mailSender.send(preparator); return emailVerificationModel; }
Password Resource Controller
The controller is a simple pass through to the Verification Token Service. Note that we always return 200 to the client regardless of whether an email address was found or not.@PermitAll @Path("tokens") @POST public Response sendEmailToken(LostPasswordRequest request) { verificationTokenService.sendLostPasswordToken(request.getEmailAddress()); return Response.ok().build(); }
Handling the Reset Request
The email sent to the user should contain a link to a static page so the user can input their new password. This, along with the token, is sent to the server.
@PermitAll @Path("tokens/{token}") @POST public Response resetPassword(@PathParam("token") String base64EncodedToken, PasswordRequest request) { verificationTokenService.resetPassword(base64EncodedToken, request.getPassword()); return Response.ok().build(); }
Again this a pass through to the Verification Token service.
@Transactional public VerificationToken resetPassword(String base64EncodedToken, String password) { Assert.notNull(base64EncodedToken); validate(passwordRequest); VerificationToken token = loadToken(base64EncodedToken); if (token.isVerified()) { throw new AlreadyVerifiedException(); } token.setVerified(true); User user = token.getUser(); user.setHashedPassword(user.hashPassword(password)); //set user to verified if not already and authenticated role user.setVerified(true); if (user.hasRole(Role.anonymous)) { user.setRole(Role.authenticated); } userRepository.save(user); return token; }
The token is matched and if it has already been verified an exception is thrown.
The user's password is hashed and reset.
Testing the API
See Part Three for configuring email settings and spring profiles
Start the application by executing:
gradle tomcatRun
Create a new user with a curl request
curl -v -H "Content-Type: application/json" -X POST -d '{"user":{"firstName":"Foo","lastName":"Bar","emailAddress":"<your email address>"}, "password":"password"}' http://localhost:8080/java-rest/user
Send a password reset request using curl
curl -v -H "Content-Type: application/json" -X POST -d '{"emailAddress":"<your email address>"}' http://localhost:8080/java-rest/password/tokens
Clicking on the link in the email will take you to a static page served from web-app.
Enter a new password and submit or alternatively cut and paste the token and use a curl statement
curl -v -H "Content-Type: application/json" -X POST -d '{"password":"password123"}' http://localhost:8080/java-rest/password/tokens/<your token>
To test that it worked you can use the login page at http://localhost:8080/java-rest/index.html or use curl
curl -v -H "Content-Type: application/json" -X POST -d '{"username":"<your email address>","password":"password123"}' http://localhost:8080/java-rest/user/login
You can also go through the complete cycle using the simple web pages provided.
So far in the series I have covered
- User sign up and login with email
- User sign up and login with OAuth
- Email Verification
- Lost Password
The next posts will focus on accessing role-based resources and session handling.
This comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete