How can I add custom validation rules to a new User registration? For example, how might we ensure that firstName is set.
At the time of writing the users.onBeforeSave event runs after validation, which means that we can’t add errors to the user easily.

The solution I’ve currently come up with is a plugin that adds custom validation rules to a user model, as well as a controller to handle the form submission.

Model

Firstly, I’ve extended the UserModel and added custom validation rules as needed.

&lt;?php

namespace Craft;

class MyPlugin_UserModel extends UserModel
{
    protected function defineAttributes()
    {
        // Get the rules defined in UserModel
        $rules = parent::defineAttributes();

        // Add custom rules
        $rules['firstName'] = array(AttributeType::String, 'required' =&gt; true);
        $rules['lastName'] = array(AttributeType::String, 'required' =&gt; true);

        // If it’s a new user, require the password
        $isNewUser = !isset($this-&gt;id);
        if ($isNewUser) {
            $rules['password'] = array(AttributeType::String, 'required' =&gt; true);
        }

        return $rules;
    }
}


Controller

Secondly, I’ve created a controller action that will handle form submissions. This populates our custom user model with the input and runs the standard validate() flow.

&lt;?php

namespace Craft;

class MyPlugin_UsersController extends BaseController
{
    protected $allowAnonymous = array('actionSaveUser');

    public function actionSaveUser()
    {
        $this-&gt;requirePostRequest();

        // Get input
        $post = craft()-&gt;request-&gt;getPost();

        // Create an instance of our model
        $user = new MyPlugin_UserModel($post);

        if (!$user-&gt;validate()) {
            // Add any errors from the validation to the user
            $user-&gt;addErrors($user-&gt;getContent()-&gt;getErrors());

            // Respond with errors
            if (craft()-&gt;request-&gt;isAjaxRequest()) {
                $this-&gt;returnJson(array(
                    'errors' =&gt; $user-&gt;getErrors(),
                ));
            } else {
                craft()-&gt;userSession-&gt;setError(Craft::t('Couldn’t save user.'));

                // Send the account back to the template
                craft()-&gt;urlManager-&gt;setRouteVariables(array(
                    'account' =&gt; $user
                ));
            }
        } else {
            // If all is well, run the default UsersController method
            craft()-&gt;runController('users/saveUser');
        }
    }
}


Template

For reference, here’s the action input in the template. Essentially, it’s just prepending the plugin handle before the default action.

&lt;input type="hidden" name="action" value="myPlugin/users/saveUser"&gt;