Friday, January 25, 2013

Writing REST services in Java: Part 5 Lost Password

Previous Post : Part Four: Facebook Authentication
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.

1 comment: