[Monday Dev Heaven] Add a Forgotten Password Functionality to Nuxeo, Part 1/2


Mon 14 May 2012 By Laurent Doguin


Ask a password reset Password Reset

This question is sometimes asked on answers or in the forum. It's a method to handle password reset for users. So I'm going to show you how I would do it. This is going to be a two part blog. Today I'll write mostly about the WebEngine module handling the password reset functionality. Next week I'll show you how to package it for Nuxeo's Marketplace.

How does it work?


Starting with something simple, we'll add a 'forgotten password' link on Nuxeo's home page. It will redirect the user to an open (i.e. no authentication needed) page asking for an email address. Once the user submits the form, we'll look in the user directory to see if the email has a corresponding user. If there's no user, we'll simply render the page again with an error message saying there's no user associated with that email address. If we do find a user, we generate a temporary link and send it to the user by email. This link has to exist for only a short period of time, for security reasons. When the user clicks on the link, he ends up on a form asking for his new password.

That's it for the simple, short user story. We'll see the details along the way. Now let's get to it :)

Ask for the reset


The first thing I did was to run the Nuxeo WebEngine project wizard with Nuxeo IDE. This gives me an empty project. I'm already able to code :) My next step is to create the form to retrieve the user email and make it available to an open URL.

This is my form:

[xhtml]
<@extends src="./base.ftl">
<@block name="title">
${Context.getMessage('label.askResetPassForm.title')}
<[email protected]>
<@block name="content">

<div class="registrationForm">
<form action="${This.path}/sendPasswordMail" method="post" enctype="application/x-www-form-urlencoded" name="submitNewPassword">
<table>
<tr>
<td class="login_label">
<span class="required">${Context.getMessage('label.registerForm.email')}</span>
</td>
<td>
<input type="text" id="EmailAddress" value="${data['EmailAddress']}" name="EmailAddress" class="login_input" isRequired="true"/>
</td>
</tr>
<tr>
<td></td>
<td>
<input class="login_button" type="submit" name="submit"
value="${Context.getMessage('label.registerForm.submit')}" />
</td>
</tr>
<#if err??>
<tr>
<td colspan="2">
<div class="errorMessage">
${Context.getMessage("label.connect.trial.form.errvalidation")}
${err}
</div>
</td>
</tr>
</#if>
<#if info??>
<tr>
<td colspan="2">
<div class="infoMessage">
${info}
</div>
</td>
</tr>
</#if>
</table>
</form>
</div>
<[email protected]>
<[email protected]>
[/xhtml]

One of the cool features that comes with WebEngine is its templating model. It gives us the ability to extend an existing Freemarker template. This way you can override only the part of the template you need. For instance, here we're extending the base.ftl template (that's because of the extends markup). This template defines at least two blocks named 'title' and 'content'. So we can redefine the content of these two using the block markup.

And this is the contribution to the openUrl extension point. It's needed to make my form available without user authentication.

[xml]

<extension
target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService"
point="openUrl">
<openUrl name="Forgotten_password">
<grantPattern>${org.nuxeo.ecm.contextPath}/site/resetPassword/</grantPattern>
</openUrl>
<openUrl name="SendPasswordMail">
<grantPattern>${org.nuxeo.ecm.contextPath}/site/resetPassword/sendPasswordMail</grantPattern>
</openUrl>
<openUrl name="EnterNewPassword">
<grantPattern>${org.nuxeo.ecm.contextPath}/site/resetPassword/enterNewPassword/.</grantPattern>
</openUrl>
<openUrl name="SubmitNewPassword">
<grantPattern>${org.nuxeo.ecm.contextPath}/site/resetPassword/submitNewPassword</grantPattern>
</openUrl>
<openUrl name="Graphical_Resources">
<grantPattern>${org.nuxeo.ecm.contextPath}/site/skin/resetPassword/.
</grantPattern>
</openUrl>
</extension>

[/xml]

Now let's get to the Java part. The doGet method will render the page I've just defined, while the sendPasswordMail method will handle the form submit. In the latter, we will do the email validation, look if we have an existing user with the given address, and if this is the case, we'll send him an email.

The validation is really simple here -- we just check if the given parameter is not null nor empty. Then if we have an email address, we go through the CreatePasswordResetLinkUnrestricted. This is the class where we look for the user, build a reset password link if the user exists, or render our page with an error message. I've tried to internationalize the email a little, but you still have to fork the code to modify the templates and subjects... I'll try to enhance that later.

[java]
@GET
public Object doGet() {
Map<String, String> data = new HashMap<String, String>();
return getView("newPasswordRequest").arg("data", data);
}

@POST
@Path(&quot;sendPasswordMail&quot;)
@Produces(&quot;text/html&quot;)
public Object sendPasswordMail() throws ClientException {
    FormData formData = getContext().getForm();
    String email = formData.getString(&quot;EmailAddress&quot;);
    if (email == null || &quot;&quot;.equals(email.trim())) {
        return redisplayFormWithErrorMessage(&quot;newPasswordRequest&quot;,
                ctx.getMessage(&quot;label.registerForm.validation.email&quot;),
                formData);
    }
    email = email.trim();
    CreatePasswordResetLinkUnrestricted runner = new CreatePasswordResetLinkUnrestricted(
            getDefaultRepositoryName(), email);
    runner.runUnrestricted();

    String errorMessage = runner.getErrorMessage();
    if (errorMessage != null) {
        return redisplayFormWithErrorMessage(&quot;newPasswordRequest&quot;,
                ctx.getMessage(errorMessage), formData);
    } else {
        String passwordResetLink = runner.getPasswordResetLink();
        String subject;
        Template template;
        if (ctx.getLocale().equals(Locale.FRENCH)) {
            template = getView(&quot;mail/passwordForgotten_fr&quot;);
            subject = &quot;Nuxeo - Votre nouveau mot de passe&quot;;
        } else {
            template = getView(&quot;mail/passwordForgotten&quot;);
            subject = &quot;Nuxeo - Your new password&quot;;
        }
        String message = template.arg(&quot;passwordResetLink&quot;,
                passwordResetLink).render();
        try {
            sendEmail(email, subject, message);
        } catch (MessagingException e) {
            // issue while sending the mail
            log.error(&quot;Sending Registration E-Mail Error&quot;, e);
            return Response.status(500).build();
        }
        return redisplayFormWithInfoMessage(&quot;newPasswordRequest&quot;,
                ctx.getMessage(&quot;label.sendPasswordMail.emailSent&quot;),
                formData);
    }
}

protected Template redisplayFormWithMessage(String messageType,
        String formName, String message, FormData data) {
    Map&lt;String, String&gt; savedData = new HashMap&lt;String, String&gt;();
    for (String key : data.getKeys()) {
        savedData.put(key, data.getString(key));
    }
    return getView(formName).arg(&quot;data&quot;, savedData).arg(messageType,
            message);
}

protected Template redisplayFormWithInfoMessage(String formName,
        String message, FormData data) {
    return redisplayFormWithMessage(&quot;info&quot;, formName, message, data);
}

protected Template redisplayFormWithErrorMessage(String formName,
        String message, FormData data) {
    return redisplayFormWithMessage(&quot;err&quot;, formName, message, data);
}

public void sendEmail(String email, String subject, String message)
        throws MessagingException {
    Composer cp = new Composer();
    Mailer mailer = cp.getMailer();
    Mailer.Message msg = mailer.newMessage();
    msg.setFrom(MAIL_FROM);
    msg.setSubject(subject);
    msg.setRecipient(RecipientType.TO, new InternetAddress(email));
    msg.setContent(message, &quot;text/html&quot;);
    msg.send();
}

[/java]

Manage the keys


Now we're able to retrieve and email address, find the corresponding user and send the email. Under the hood, the CreatePasswordResetLinkUnrestricted generates a unique key, stored in a directory along with the user email address and the current date. A directory is a good fit to store our temporary keys. I will delete every one-day-old key using Nuxeo's event system.

Let's add a contribution to the scheduler service. It will raise the cleanResetPassKeys event every day at 1am.

[xml]
<extension
target="org.nuxeo.ecm.platform.scheduler.core.service.SchedulerRegistryService"
point="schedule">
<schedule id="cleanResetPassKeys">
<username>Administrator</username>
<eventId>cleanResetPassKeys</eventId>
<eventCategory>default</eventCategory>
<cronExpression>0 0 1 ?</cronExpression>
</schedule>
</extension>
[/xml]

So what we need to do now is add a listener for the cleanResetPassKeys event.

Here's the code of my listener. It's straightforward. The simpleDate String is the previous day's date, formatted the same way we stored it in the directory. We use it as a directory filter. This way we can retrieve all keys created the day before and delete them. What I'd like to do in the future is make this configurable in the Admin Center. That way, the administrator could choose the time to live for the key.

[java]
public class CleanForgottenPasswordKeysListener implements EventListener {

public static final Log log = LogFactory
        .getLog(CleanForgottenPasswordKeysListener.class);

@Override
public void handleEvent(Event event) throws ClientException {
    SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyMMdd&quot;);
    Calendar yesterday = Calendar.getInstance();
    yesterday.add(Calendar.DATE, -1);
    String simpleDate = sdf.format(yesterday.getTime());
    DirectoryService ds;
    try {
        ds = Framework.getService(DirectoryService.class);
    } catch (Exception e) {
        throw new RuntimeException(&quot;Could not find DirectoryService&quot;, e);
    }
    Session session = ds.open(&quot;resetPasswordKeys&quot;);
    Map&lt;String, Serializable&gt; filter = new HashMap&lt;String, Serializable&gt;();
    filter.put(&quot;creationDate&quot;, simpleDate);
    DocumentModelList keysToRemove = session.query(filter);
    for (DocumentModel key : keysToRemove) {
        session.deleteEntry(key);
    }
    session.close();
}

}
[/java]

That's it for today. In the second part I'll tell you how to actually reset the password. Or if you are curious you can already checkout the source code on Github. I will also show you how to make a Marketplace package of this. See ya' on Friday :)


Category: Product & Development
Tagged: Java, Monday Dev Heaven, Nuxeo IDE