Creating users with email validation

Description

Users can be created in three ways:

  • by adding user details in the configuration file

  • by implementing a user registration functionality used by admins

  • by allowing any user to self register

Adding users via configuration has already been covered in Overview.

If the user details have already been provided by some trusted means, an admin could input those details and create the user. The process may require the target user to click on a validation link in a confirmation email but this isn’t strictly required. Reading user details could be implemented via a protected form or a csv file upload.

When users are free to register on their own, these elements should be provided:

  • a public registration form

  • a confirmation email with a secret confirmation link

  • a handler for the confirmation link where users are saved to database

Registration request

If user registration requires clicking on a validation link from an email, the user information must be stored in the database for later. The YadaRegistrationRequest.java class can be used for that. It can store some basic information like email and password and should be subclassed in order to store application-specific user information like name and surname or a newsletter subscription flag. An example implementation is provided in the application entity package and should be customised as needed.

Registration form

A registration link should open a page/modal with a registration form. The form can use the YadaRegistrationRequest subclass (MyRegistrationRequest in the example) as a backing bean or the UserProfile directly if a confirmation email is not required.

@RequestMapping("/registerPage")
public String registerPage(MyRegistrationRequest myRegistrationRequest) {
	return "/register";
}

The registration form can use a hidden "antispam" field that would be filled by spam scripts only, so that the most basic spam attacks can be easily prevented. In the example implementation the field "username" must be left blank in order for registration to be successful. The real username is stored in the "email" field.

<form th:action="@{/signup}" th:object="${myRegistrationRequest}" role="form" method="post" id="registrationForm">
	<fieldset>
		<!--/* The username field is hidden and used to detect spam */-->
		<label for="username" class="hidethis">Username</label>
		<input type="text" class="hidethis" id="username" name="username" th:field="*{username}">
		<label for="email">E-Mail</label>
		<input type="email" id="email" th:field="*{email}" required="required" autofocus="autofocus">
		<label for="password">Password</label>
		<input type="password" th:field="*{password}" required="required">
		<label for="confirmPassword">Confirm Password</label>
		<input type="password" id="confirmPassword" name="confirmPassword">
		...

The password confirmation field (optional) can be enabled with

<script>
	yada.enablePasswordMatch($("#registrationForm"));
</script>

This will apply the classes yada-password-mismatch and has-error (for Bootstrap 4) to the form when the input fields named "password" and "confirmPassword" don’t match. In such case it will also disable the form submit button. The above classes can be used for example to make visible some predefined error or change the color of the labels.

The controller that handles the registration form submission should perform some validation on the application-specific fields then call the YadaRegistrationController.handleRegistrationRequest() method to finalise the operation. A default implementation is provided in the RegistrationController.java class.

The handleRegistrationRequest() method performs some simple validation on the email address syntax and checks if the address has been blacklisted. Blacklisting is done by specifying regex patterns in the configuration as shown in this example:

	<email>
		...
		<blacklistPattern>.*invalid.*</blacklistPattern>
		<blacklistPattern>.*@mailinator.com</blacklistPattern>
	</email>

It also checks if the user exists and the password is within the configured length. Validation error messages are internationalised with the following keys:

yada.form.registration.email.invalid = Invalid email address
yada.form.registration.username.exists = Email already registered
yada.form.registration.password.length = The password must be between {0} and {1} characters long.
yada.form.registration.email.failed = Sending the registration confirmation email has failed. Please try again later

If validation fails, the method returns false and sets the relevant flags on the BindingResult.

Confirmation email

The email sent to the user is /src/main/resources/template/email/registrationConfirmation.html. The default implementation should be customised to the application needs.

For localisation purposes, the system will first check for an email file ending in _<language> like registrationConfirmation_de.html, then fall back to the plain version without language if that file is not found. This means that email messages can be localised by creating the locale-specific file for each supported language other than the default one.

The subject for the email is defined by the message.properties key `email.subject.registrationConfirmation'. It will receive the email timestamp as parameter 0 (can be omitted). Example:

email.subject.registrationConfirmation = Registration ({0})

Confirmation handler

The default link in the confirmation email has the format /registrationConfirmation/{token} where {token} is a unique identifier saved in the database. This can be overridden with the config/security/registration/confirmationLink configuration entry:

<security>
	<registration>
		<confirmationLink>/my/registrationAccept</confirmationLink>
	</registration>

The controller that handles the confirmation request can conveniently invoke the yadaRegistrationController.handleRegistrationConfirmation() method and handle the result according to the application needs:

	@RequestMapping("/registrationConfirmation/{token}")
	public String registrationConfirmation(@PathVariable String token, Model model, Locale locale, RedirectAttributes redirectAttributes, HttpServletRequest request, HttpSession session) {
		YadaRegistrationOutcome<UserProfile> outcome = yadaRegistrationController.handleRegistrationConfirmation(token, new String[]{config.getRoleName(ROLE_USER_ID)}, locale, session, UserProfile.class);
		switch (outcome.registrationStatus) {
		case LINK_EXPIRED:
			yadaNotify.titleKey(redirectAttributes, locale, "registration.confirmation.expired.title").error().messageKey("registration.confirmation.expired.message").add();
			return yadaWebUtil.redirectString("/", locale);
		case USER_EXISTS:
			redirectAttributes.addAttribute("email", outcome.email);
			yadaNotify.titleKey(redirectAttributes, locale, "registration.confirmation.existing.title").error().messageKey("registration.confirmation.existing.message", outcome.email).add();
			return yadaWebUtil.redirectString("/passwordReset", locale);
		case OK:
			yadaNotify.titleKey(redirectAttributes, locale, "registration.confirmation.ok.title").ok().messageKey("registration.confirmation.ok.message", outcome.email).add();
			log.info("Registration of '{}' successful", outcome.email);
			return yadaWebUtil.redirectString("/", locale);
		case ERROR:
		case REQUEST_INVALID:
			yadaNotify.titleKey(redirectAttributes, locale, "registration.confirmation.error.title").error().messageKey("registration.confirmation.error.message").add();
			return yadaWebUtil.redirectString("/", locale);
		}
		log.error("Invalid registration state - aborting");
		throw new YadaInvalidUsageException("Invalid registration state");
	}

It is in this method that any application-specific data gathered from the registration form should be stored in the user profile.

TODO: show the updated code