2013/04/07

Adding Signup email confirmation to AppFuse

Introduction


In todays SaaS world, user signup usually requires email confirmation because of continuous events that trigger new email messages to users.

AppFuse is a RAD Java web app framework, supporting a multitude of web frameworks: Spring MVC, Struts2, JSF, Tapestry, and there are more to come.

Let's implement a user signup email confirmation service for AppFuse. This can be adapted to any other Java web app framework.

Initial Web App

From the AppFuse quickstart page, I copy the maven command to generate an initial Struts 2 app from AppFuse archetypes:

mvn archetype:generate -B 
 -DarchetypeGroupId=org.appfuse.archetypes 
 -DarchetypeArtifactId=appfuse-basic-struts-archetype 
 -DarchetypeVersion=2.2.1 
 -DgroupId=com.operatornew 
 -DartifactId=signup 
 -DarchetypeRepository=http://oss.sonatype.org/content/repositories/appfuse

As we will be updating the User services, we generate the full source code from the newly created signup directory:

mvn appfuse:full-source

User email verification

 How can we verify a new user's email?

1. A new user signs up and fills in an email address which we want to verify as valid.

2. After the user submits their data, we'll generate a unique and difficult to guess token for each that signs up. The new user won't be able to log in until they complete their email verification process.

3. We'll send an email with a URL from our app which will include this generated token. As AppFuse supports multi-language, we'll generate the email in the active locale.

4. When the new user receives the confirmation email, they will be able to visit the included URL with unique token to say "Hey, it's me. I've received your difficult-to-guess token at the email address I gave you". We will mark then this user as confirmed.

User Signup Confirmation: Service and Model Layers

Ok. Let's add a new Java interface for our new Signup confirmation service. The service will be responsible for starting a user's data confirmation process and confirming the user's data. We will apply it to email verification, but it could be applied to mobile phone number verification as well.

public interface SignupConfirmService {

    User doConfirm(User user);

    /**
     * Starts a user Confirmation procedure
     *
     * @param user the User whose data needs to be confirmed. The user's signupDate will be updated accordingly
     * @param context the Web App context from which the locale and URL will be obtained
     * @return
     */
    User startConfirm(User user, WebAppContext context);

}

Hold on. What is this WebAppContext type? We'll need to include our web app URL in the generated email. As we'll implement the confirmation in the service level, we'll avoid to add an unnecessary dependency to servlet classes. After all, we're working at the service layer.


The WebAppContext type will be an abstract class:

public abstract class WebAppContext {
    private Locale locale;

    public WebAppContext(Locale locale) {
        this.locale = locale;
    }

    public Locale getLocale() {
        return locale;
    }

    abstract public String getAppUrl();
}

Updating our Model

We'll add to our User class three pieces of information:
  • a signup date
  • an email confirmation token
  • a confirmation date
We add the dates for administration purposes.

public class User extends BaseObject implements Serializable, UserDetails {

    ...

    private Date signupDate;
    private String emailConfirmToken;
    private Date confirmDate;

    ...

    @Column(name="signup_date", nullable=false)
    @Temporal(TemporalType.TIMESTAMP)
    @Field
    public Date getSignupDate() {
        return signupDate;
    }

    public void setSignupDate(Date signupDate) {
        this.signupDate = signupDate;
    }

    @Column(name="confirm_token", length=32)
    public String getEmailConfirmToken() {
        return emailConfirmToken;
    }

    public void setEmailConfirmToken(String confirm) {
        this.emailConfirmToken = confirm;
    }

    @Column(name="confirm_date")
    @Temporal(TemporalType.TIMESTAMP)
    @Field
    public Date getConfirmDate() {
        return confirmDate;
    }

    public void setConfirmDate(Date confirmDate) {
        this.confirmDate = confirmDate;
    }
}

Implementing our new Service

We can now write an implementation for our new SignupConfirmService to verify a new user's email address.
Our implementation will use AppFuse MailEngine service to send email, a ResourceBundle for mail subject i18n, and the Java SecureRandom class to generate a unique and difficult to guess token for each new user.

public class SignupConfirmEmail implements SignupConfirmService {

    // For the secure random generation of tokens
    private SecureRandom random = new SecureRandom();

    // AppFuse email service, injected by Spring
    private MailEngine mailEngine;
    // AppFuse email prototype, injected by Spring
    private SimpleMailMessage mailMessage;

    // i18n for the subject's email. Configurable in spring bean, with a default bundle name
    private String resourceBundleName = "MailResources";
    private ResourceBundle rb;


    .... // spring bean setters omitted

    @Override
    public User startConfirm(User user, WebAppContext context) {
        String code = new BigInteger(130, random).toString(32);
        user.setEmailConfirmToken(code);
        mailMessage.setTo(user.getFullName() + "<" + user.getEmail() + ">");
        mailMessage.setSubject(getFromResourceBundle("email.signup.subject", context.getLocale()));
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("userFirstName", user.getFirstName());
        model.put("appURL", context.getAppUrl());
        model.put("signupConfirmURL", context.getAppUrl() + "/account/confirm?confirm="+user.getEmailConfirmToken());
        mailEngine.sendMessage(mailMessage, "signupConfirm.vm", model);
        return user;
    }

    @Override
    public User doConfirm(User user) {
        if (!user.isEnabled()) {
            // has not been confirmed yet:
            user.setEnabled(true);
            user.setConfirmDate(new Date());
        }
        return user;
    }

    private String getFromResourceBundle(String key, Locale locale) {
        if (rb == null) {
            try {
                rb = ResourceBundle.getBundle(resourceBundleName, locale);
            } catch (MissingResourceException ex) {
                rb = ResourceBundle.getBundle("com.operatornew." + resourceBundleName, locale);
            }
        }
        String str = rb.getString(key);
        return str;
    }

We can now define the SignupConfirmService implementation as a Spring bean in our applicationContext-service.xml file:

<bean id="signupConfirmService" class="com.operatornew.service.impl.SignupConfirmEmail">
    <property name="mailEngine" ref="mailEngine" />
    <property name="mailMessage" ref="mailMessage" />
</bean>

Next, we'll modify AppFuse's UserManager as follows:
  • add a signup() method for user signup, which will check if there is a signup confirmation service in place
  • we can make the signup confirmation service globally optional with a configuration parameter
  • add a confirmSignup() method for user signup confirmation

The implementation could be this one:

public User signup(User user, WebAppContext app) throws UserExistsException {
    // First save user to check there is no conflict with unique fields
    user = saveUser(user);

    if (signupConfirmService != null) {
        signupConfirmService.startConfirm(user, app);
        if (enableAccountAfterSignupConfirm) {
            user.setEnabled(false);
        }
    }

    // save updated fields
    return save(user);
}

public User confirmSignup(String token) throws UserSignupTokenNotFoundException {
    User user = userDao.getUserFromSignupToken(token);
    if (signupConfirmService != null) {
        user = signupConfirmService.doConfirm(user);
        return userDao.saveUser(user);
    } else {
        throw new RuntimeException("signupConfirmService null while trying to confirm signUp for token " + token);
    }
}

For confirmSignup, we need to add a method to userDao to retrieve a user by their signup token:

public User getUserFromSignupToken(String token) throws UserSignupTokenNotFoundException {
    List users = getSession().createCriteria(User.class).add(Restrictions.eq("emailConfirmToken", token)).list();
    if (users == null || users.isEmpty()) {
        throw new UserSignupTokenNotFoundException("user token'" + token + "' not found...");
    } else {
        return (User) users.get(0);
    }
}

Testing the new Service

If we run our tests now:

mvn test

We see we have broken something:

...
Results :

Failed tests:
  testSave(com.operatornew.webapp.action.SignupActionTest)

Tests in error:
  testAddAndRemoveUser(com.operatornew.service.UserManagerTest)

Tests run: 71, Failures: 1, Errors: 1, Skipped: 0

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] There are test failures.

The surefire junit report for one of the failing tests looks like this:
...
Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.584 sec <<< FAILURE!
testAddAndRemoveUser(com.operatornew.service.UserManagerTest)  Time elapsed: 0.082 sec  <<< ERROR!
com.operatornew.service.UserExistsException: User 'john' already exists!
 at com.operatornew.service.impl.UserManagerImpl.saveUser(UserManagerImpl.java:143)
...

However, it does not make sense that a user already exists after our changes. Upon inspection of the test log in the console, I can spot the error:

...
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'signup_date' cannot be null
We fix this by setting a signupDate if it is null in saveUser() method:
...
if (user.getSignupDate() == null) {
    user.setSignupDate(new Date());
}

We rerun our tests:

...
Results :

Tests in error:
  testSave(com.operatornew.webapp.action.SignupActionTest)

Tests run: 71, Failures: 0, Errors: 1, Skipped: 0

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] There are test failures.

After inspecting the surefire report, we can see this:

-------------------------------------------------------------------------------
Test set: com.operatornew.webapp.action.SignupActionTest
-------------------------------------------------------------------------------
Tests run: 5, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.168 sec <<< FAILURE!
testSave(com.operatornew.webapp.action.SignupActionTest)  Time elapsed: 0.064 sec  <<< ERROR!
org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
 at org.springframework.dao.support.DataAccessUtils.requiredSingleResult(DataAccessUtils.java:71)
 at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:730)
 at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:749)
 at com.operatornew.dao.hibernate.UserDaoHibernate.getUserPassword(UserDaoHibernate.java:109)
...
 at com.operatornew.service.impl.UserManagerImpl.saveUser(UserManagerImpl.java:123)
 at com.operatornew.service.impl.UserManagerImpl.signup(UserManagerImpl.java:88)
...

Our implementation is failing in the new UserManagerImpl signup() method, when saveUser() is called the second time to save fields updated by the SignupConfirmationService implementation. The saveUser() method eventually calls UserDao's getUserPasword() to check if the password needs to be re-encrypted. But getUserPassword() is annotated not to support propagation of transactions, whichs makes it fail because our new user is created within a transaction that has not finished yet:

public interface UserDao extends GenericDao<User, Long> {
    ...
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    String getUserPassword(Long userId);
}

The second saveUser method call in our new signup method does not need to re-encrypt user password clearly. We can fix it with a private method that just saves the user instance without checking for password encoding, and a bit of refactoring to avoid duplicated code:

public User signup(User user, WebAppContext app) throws UserExistsException {
    // First save user to check there is no conflict with unique fields
    user = saveUser(user);

    if (signupConfirmService != null) {
        signupConfirmService.startConfirm(user, app);
        if (enableAccountAfterSignupConfirm) {
            user.setEnabled(false);
        }
    }
    // save updated fields
    return saveUserNoPwdEncoding(user);
}

public User saveUser(User user) throws UserExistsException {
    ...
    return saveUserNoPwdEncoding(user);
}

private User saveUserNoPwdEncoding(User user) throws UserExistsException {
    if (user.getSignupDate() == null) {
        user.setSignupDate(new Date());
    }

    try {
        return userDao.saveUser(user);
    } catch (Exception e) {
        e.printStackTrace();
        log.warn(e.getMessage());
        throw new UserExistsException("User '" + user.getUsername() + "' already exists!");
    }
}

All our tests now pass.

We can now write a new test for our SignupConfirmService class. We'll use Wiser to mock smtp server, and verify that startConfirm() sets a value on user's emailConfirmToken, that an email is sent and that the email's body contains the generated token:

public class SignupConfirmServiceTest extends BaseManagerTestCase {
    private Log log = LogFactory.getLog(SignupConfirmServiceTest.class);
    @Autowired
    MailEngine mailEngine;
    JavaMailSenderImpl mailSender = new JavaMailSenderImpl();

    @Autowired
    private SignupConfirmService signupConfirmService;
    private User user;

    @Before
    public void setUp() {
        mailSender.setHost("localhost");
        mailEngine.setMailSender(mailSender);
    }

    @After
    public void tearDown() {
        mailEngine.setMailSender(null);
    }

    @Test
    public void testStartConfirm() throws Exception {
        // mock smtp server
        Wiser wiser = new Wiser();
        // set the port to a random value so there's no conflicts between tests
        int port = 2525 + (int)(Math.random() * 100);
        mailSender.setPort(port);
        wiser.setPort(port);
        wiser.start();

        user = new User();

        // call populate method in super class to populate test data
        // from a properties file matching this class name
        user = (User) populate(user);
        assertNull(user.getEmailConfirmToken());

        user = signupConfirmService.startConfirm(user, new WebAppContext(new Locale("en")) {
            public String getAppUrl() {
                return "http://localhost:8080";
            }
        });
        wiser.stop();
        assertNotNull(user.getEmailConfirmToken());

        assertTrue(wiser.getMessages().size() == 1);
        WiserMessage wm = wiser.getMessages().get(0);
        assertThat((String)wm.getMimeMessage().getContent(), containsString(user.getEmailConfirmToken()));
    }
}

It will initially fail as we need to add resources for email subject i18n and the Velocity templates. In MailResources.properties:

email.signup.subject=Confirm your email Address

In signupConfirm.vm Velocity template:

Hello ${userFirstName},

To enable your account, please click in the following link or copy it onto the address bar of your favourite browser.

${signupConfirmURL}

After that, the test passes.

User Signup Confirmation: Web Layer

After implementing the service layer, we can now implement the web layer.

When a new user clicks on the sing up link, a page with a fill-in form is showed and the user enters their info.

After completion of the form, the user will press the signup button and, if configured to confirm email before the account is enabled, a page will be displayed informing the user that an email has been sent to their email address and they need to confirm their account by following email instructions.

In AppFuse with Struts2, the SignupAction class implements the Struts2 action for Signup. The save() method currently saves the new signed up user, sends them an email of signup welcome and logs the new user in.

We will change the implementation by calling the new UserManager.signup() method, eliminate the welcome email and only will log in the new user if the app is NOT configured to confirm email before the account is enabled.

public String save() throws Exception {
    // Set the default user role on this new user
    user.addRole(roleManager.getRole(Constants.USER_ROLE));

    try {
        // call signup, passing current locale and a new anonymous inner class returning the web app URL
        user = userManager.signup(user, new WebAppContext(this.getLocale()) {
            public String getAppUrl() {
                return RequestUtil.getAppURL(getRequest());
            }
        });

    } catch (AccessDeniedException ade) {
        // thrown by UserSecurityAdvice configured in aop:advisor userManagerSecurity
        log.warn(ade.getMessage());
        getResponse().sendError(HttpServletResponse.SC_FORBIDDEN);
        return null;
    } catch (UserExistsException e) {
        log.warn(e.getMessage());
        List<Object> args = new ArrayList<Object>();
        args.add(user.getUsername());
        args.add(user.getEmail());
        addActionError(getText("errors.existing.user", args));

        // redisplay the unencrypted passwords
        user.setPassword(user.getConfirmPassword());
        return INPUT;
    }

    if (userManager.isEnableAccountAfterSignupConfirm()) {
        // user signup needs confirmation to be enabled
        getSession().setAttribute(Constants.REGISTERED, user.getEmail());
        return "confirm";
    } else {
        saveMessage(getText("user.registered"));
        getSession().setAttribute(Constants.REGISTERED, Boolean.TRUE);

        // log user in automatically
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                user.getUsername(), user.getConfirmPassword(), user.getAuthorities());
        auth.setDetails(user);
        SecurityContextHolder.getContext().setAuthentication(auth);

        return SUCCESS;
    }
}

We will add two new struts actions:
  • account/needconfirm will inform the user that they need to confirm their account by following email instructions
  • account/confirm will try to confirm a user account with the supplied confirmation token. The result can either be successful or unsuccessful, informing the user accordingly
 The relevant Struts 2 xml configuration file for the new Struts 2 actions:

<action name="saveSignup" class="signupAction" method="save">
    <result name="cancel" type="redirect">/home</result>
    <result name="input">/WEB-INF/pages/signup.jsp</result>
    <result name="success" type="redirectAction">home</result>
    <result name="confirm" type="redirectAction">account/needconfirm</result>
</action>

<action name="account/needconfirm">
    <result>/WEB-INF/pages/signupNeedConfirm.jsp</result>
</action>

<action name="account/confirm" class="signupAction" method="confirm">
    <result name="success">/WEB-INF/pages/signupConfirmed.jsp</result>
    <result name="error">/WEB-INF/pages/signupConfirmError.jsp</result>
</action>

The page to inform the user that they need to confirm their account by following the email instructions can be like this signupNeedConfirm.jsp:

<%@ include file="/common/taglibs.jsp" %>
<%@page import="com.operatornew.Constants" %>
<% String email = (String)session.getAttribute(Constants.REGISTERED);
%>

<head>
    <title><fmt:message key="signupNeedConfirm.title"/></title>
</head>

<body class="signup-confirm"/>

<div class="span2">
</div>

<div class="span8 dialog-info">
<div class="alert alert-block alert-info">
        <h2><fmt:message key="signupNeedConfirm.heading"/></h2>
        <fmt:message key="signupNeedConfirm.message">
            <fmt:param><%= email %></fmt:param>f
        </fmt:message>
    </div>
</div>

<div class="span2"/>

It displays a message informing to which email has been sent the message explaining the user what they need to perform in order to complete the signup process.

The other two new jsp pages are very similar to this one.

Not least important is making the new action URL's publicly accessible to any user. For this, we update Spring's security config file security.xml like this:

<http auto-config="true">
    <intercept-url pattern="/account/confirm*" access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
    <intercept-url pattern="/account/needconfirm*" access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
    ...
</http>


Screenshots

Here are some screenshots of the added functionality.

Page requesting the user to confirm their email:
Email received by the user:



Page showing successful confirmation and account activation:


Page showing wrong account activation:

Sources


Sources can be found here.

1 comment:

  1. thank you so much for this. you're superb!!

    ReplyDelete