Monday, January 11, 2010

Validating User Emails, part 2

The last two steps of validating user emails involved changing the profile servlet to display the link to resend the verification email when they already have an unverified email, and to send the verification email when they change their email.

Since both of those require sending the verification email, I'll put that in a utility class. I already have MailUtil, and this is a special sort of email, so I'll stick it there.

The main problem so solve before I can send the verification email is generating the unique verification code. I mentioned that MD5 is fine to use here, so we'll use that. The MessageDigest class in Java is what we use to generate the MD5 value, like this:


MessageDigest md5 = null;

try
{
md5 = MessageDigest.getInstance("md5");
} catch (NoSuchAlgorithmException e)
{
// Should never happen, but just in case
return "Unable to access md5 digest algorithm";
}

byte [] digest = md5.digest(user.getEmail ().getBytes());


Note the exception handling...that exception should never happen, because MD5 is a standard digest algorithm. But since the exception can be thrown, Java requires we handle it. This assumes the method returns a String that's an error message if something went wrong.

The byte array we get from the MessageDigest is not suitable for putting into an email yet. We need to convert it into hexadecimal first, like this:


String code = "";

for (byte element : digest)
code += Integer.toHexString(element & 0xFF);


If you're wondering about the use of "& 0xFF", it's because of the way that integral values are stored in Java. Think in terms of twos-complement sign extending from 1 byte to 4 bytes, and getting a bunch of extra 1s. We strip off anything extra by anding it with a single byte of 1s.

And if none of that makes sense, just believe me that you need it (or don't believe me, and remove it, and you'll see the difference in the hexadecimal generated).

So what I end up with is a hexadecimal string suitable for use as a verification code.

I also want to store the email I'll send as a Freemarker template, rather than have it embedded in Java code. This means a couple of things: I'll need to pass the Template object into the MailUtil method, and when I process the template I'll need to use a StringWriter so I can get the results as a String. The StringWriter serves in place of how we've been using PrintWriter for the templates.

I'll leave that, and finishing off the send verification email method as an exercise for the reader.

The last bit I'll mention here is that we'll need to include a link to the servlet we wrote last post that handles the verification clicks in the email. In the template we'll use something like this in the href arg of an a tag:


http://localhost:8080//validate-email?code=${code}


We'll use localhost for testing, but will want to change that when we're deploying the application. For those keeping track, we now have two places where we've embedded localhost and made notes to change it when we deploy.

Might be a very good idea to include a programmatic way to switch this depending on whether the application is running on the server or on the local machine. You can check that like this:


if (SystemProperty.environment.get().contains("Development"))


That if statement is true when you're running on the local host, and false otherwise. Use that if statement at appropriate points to change the host name used in URLs. The SystemProperty class used there is an App Engine provided one.

(Update: I just realized that the SystemProperty class doesn't exist on the App Engine version installed in the computer lab. So we'll likely update to the most recent version at some point, to keep things in sync with what students would have at home.)

Okay, so if you've done the exercises left for the reader, you have a MailUtil method that will send a verification email. We need to invoke that method in the profile servlet when the user changes their email, using code like this:


private String sendVerificationEmail (User user) throws IOException
{
Configuration config = ConfigFactory.get(getServletContext ());
Template template = config.getTemplate("emailVerify.ftl");

try
{
MailUtil.sendVerificationEmail(user, template);
}
catch (Exception e)
{
return e.getMessage ();
}

return "";
}


This is in the profile servlet, and will be called from the code that updates the email in the User. I'm not showing those parts, because they're basically the same as we did for updating the name.

The last bit we need is to display the link to resend the verification email when the user is editing their profile and their email isn't verified.

I'm going to add a query parameter to the ProfileServlet that will tell it to resend the verification email. It'll be something like this:


if (req.getParameter("resend") != null)
{
// Only resend if the email isn't verified already
if (! user.getEmailVerified())
{
message = sendVerificationEmail (user);
}
}


Now that the profile servlet will handle that query parameter, put the link in when the email isn't verified. I'll add the link to the ProfileLoggedIn template, just as a variable, e.g. ${resendLink}. Then in the profile servlet, when the email isn't empty but isn't verified, I'll set that variable to the resend link in the data model. When the email is empty, or verified, I'll set that variable to an empty string.

Since that uses only techniques we've already seen, I'll leave that as an exercise for the reader. Note also that I haven't tested the above, so it might very well blow up. When I get a chance to test it I'll update this post to reflect any fixes needed.

2 comments:

  1. Really well written and useful article.

    Maybe a stupid question but reading the documentation I understand that MessageDigest will always return the same hash key when providing the same input.

    If this is correct, the statement in the code "md5.digest(user.getEmail ().getBytes());" will always return the same value for a given email.

    This means that if a user detects that md5 is used to compute the verification code, it can guess the verification code for another email.

    Am I missing anything ?

    ReplyDelete
  2. Good point; normally you'd have a salt, an arbitrary string unique to your application, added to the email. That way an attacker would need to know both the email and the salt.

    ReplyDelete