Server-Side Data Validation

In the client-side validation activity you read that client-side validation can be bypassed. When tools like the novalidate bookmarklet are used, native HTML validation is disabled. You need to also recognize that even if you use JavaScript validation instead, that JavaScript can be disabled in the browser. Therefore, client-side validation is not enough!

Server-side validation is implementing equivalent checks on the server in order to double-check incoming data to make sure that it meets the same requirements that we asked the browser to check. That is what we will do in this activity.

Example Video

The video provides a general overview of the activity, but does not contain the detail needed to complete each process. Watch the video to obtain a general idea, but follow the written steps to complete the activity. This is the Transcript of the video.

New Package

We'll install a new package that should help us in the validation process. Validating and sanitizing data is important to protect the application and the database. Install express-validator - a middleware package that contains validator and sanitizer functionality.

  1. Open a VSC terminal.
  2. Type,
    pnpm add express-validator
    , press "Enter".
  3. When done, close the terminal.

Sanitize

Sanitization functions remove characters or convert potentially harmful characters to entities that are not executable. Think of washing your hands. The soap is meant to sanitize your skin, to remove germs or organisms that could cause you to get sick. Some functions built into the express validator do the same thing - they remove undesirable items from incoming data.

Validate

In simple terms, to validate is to ensure that what we expect to receive is what we actually receive. A way of making sure that an email address is one, and that a password meets the stated requirement.

Be aware that adding sanitization rules without validation rules will NOT work. If you have sanitization in place, you must have at least one validation check as well.

Express Validator Documentation

The Express Validator documentation is extensive. Open it and look specifically at the "Sanitization", "Validation Chain API" and "validationResult()" items in the left navigation bar. As you look at the documentation, you will find that the Express Validator is built on top of the validator.js library.

Validator.js Documentation

The actual sanitization and validation items belong to the Validator.js library. Open the documentation for the validator library and look through the sanitizer and validator functions listed. We will not use all of these, but it is good to be familiar with what is available, so you know where to look when you need these tools in the future.

Important!

Once the server-side validation is in place, and errors can be displayed in views, it is important that the following code be added to each res.render data object when the view is initially delivered: errors: null,

For example, in the accountController, the "Deliver registration view" function would be altered to look like this:

/* ****************************************
*  Deliver registration view
* *************************************** */
async function buildRegister(req, res, next) {
  let nav = await utilities.getNav()
  res.render("account/register", {
    title: "Register",
    nav,
    errors: null,
  })
}

If this is missed, the EJS view engine could throw an error because an "errors" variable is expected, and not found.

Implementation

Declare the Registration Rules

Sanitization and validation should occur in that order. 1) Sanitize (remove unwanted characters) incoming data first, then 2) validate (check) what is left and see if it is what's expected and/or allowed.

To keep our application code manageable, we will write our checks in their own document, then implement it into the applicable route. [Note: validation occurs prior to data reaching the controller, that is why the checks will be implemented into the route.]

You are STRONGLY ENCOURAGED to open the links (see above) to the Express-Validator and Validator.js documentation while implementing the sanitizing and validation functions below.

  1. We'll begin with the registration data.
  2. Create a new JavaScript file in the utilities folder, named account-validation.
  3. At the top of the new file you'll require the utilities > index.js file and the express validator. Finally, you'll create a "validate" object, as shown:
    const utilities = require(".")
      const { body, validationResult } = require("express-validator")
      const validate = {}
  4. In line 2, the express validator contains multiple tools, two of which we have indicated we wish to use: body and validationResult. The body tool allows the validator to access the body object, which contains all the data, sent via the HTTPRequest. The validationResult is an object that contains all errors detected by the validation process. Thus, we use the first tool to access the data and the second to retrieve any errors.
  5. Next, we will create a function that will return an array of rules to be used when checking the incoming data. Each rule focuses on a specific input from the registration form. We will build the entire function first, then examine each rule individually.
  6. /*  **********************************
      *  Registration Data Validation Rules
      * ********************************* */
      validate.registationRules = () => {
        return [
          // firstname is required and must be string
          body("account_firstname")
            .trim()
            .escape()
            .notEmpty()
            .isLength({ min: 1 })
            .withMessage("Please provide a first name."), // on error this message is sent.
      
          // lastname is required and must be string
          body("account_lastname")
            .trim()
            .escape()
            .notEmpty()
            .isLength({ min: 2 })
            .withMessage("Please provide a last name."), // on error this message is sent.
      
          // valid email is required and cannot already exist in the DB
          body("account_email")
          .trim()
          .escape()
          .notEmpty()
          .isEmail()
          .normalizeEmail() // refer to validator.js docs
          .withMessage("A valid email is required."),
      
          // password is required and must be strong password
          body("account_password")
            .trim()
            .notEmpty()
            .isStrongPassword({
              minLength: 12,
              minLowercase: 1,
              minUppercase: 1,
              minNumbers: 1,
              minSymbols: 1,
            })
            .withMessage("Password does not meet requirements."),
        ]
      }
  7. An Explanation

    • Lines 1-3 - a multi-line comment
    • Line 4 - creates and opens an anonymous function and assigns it to the "registrationRules" property of the "validate" object.
    • Line 5 - the "return" command and the opening of the array of checks for the incoming data.
    • Line 6 - a single line comment for the check that begins on the next line.
    • Line 7 - looks inside the HTTPRequest body for a name - value pair, where the name is "account_firstname". Remember the data trail concept? The form input has been given the same name as the column in the database table.
    • Lines 8-9 - .trim() is a sanitizing function used to remove whitespace (empty characters) on either side of the incoming string. .escape() finds any special character and transform it to an HTML Entity rendering it not operational as code.
    • Lines 10-11 - .notEmpty() is a validatator ensuring that a value exists. .isLength() is a validatator checking for a specified length requirement. In our code, the option is an object that declares the minimum required length of the first name must be 1 character.
    • Line 12 - .withMessage() is a function that allows an error message, specific to this rule, to be declared if the requirements are not met. If this custom message were not provided a default message of "invalid value" would be provided.
    • Line 13 - left intentionally blank.
    • Lines 12-20 - a rule of the last name, that is nearly identical to that for the first name.
    • Line 21 - left intentionally blank.
    • Line 22 - a comment introducing the email check.
    • Line 23 - indicates that the "account_email" name - value pair is to be located in the HTTPRequest body.
    • Lines 24-26 - sanitizers and a validator, the same as those for the first and last names.
    • Line 27 - isEmail() is a validation function that checks the string for characters that should be present in a valid email address. Check the validator.js documentation for additional options and default settings.
    • Line 28 - normalizeEmail() is a sanitization function that makes the email all lowercase, as well as additional alterations to "canonicalize" an email. Refer to the validator.js documentation for default settings and options. Note that the official documentation specifies that the checks on lines 27 and 28 must be in this order.
    • Line 29 - provides a custom error message for the email data.
    • Line 30 - left intentionally blank.
    • Line 31 - a comment for the password check
    • Line 32 - begins the password check process.
    • Lines 33-34 - same sanitizer and validator as previous inputs.
    • Line 35 - isStrongPassword() is a function for checking a password string to meet specific requirements to be considered a strong password. By default, the function returns a boolean - True or False. Can also have an option added to return a strength score.
    • Line 36 - a name - value pair within the options object that indicates the minimum length of the password must be 12 characters.
    • Line 37 - a name - value pair within the options object that indicates the minimum number of lowercase alphabetic characters must be one.
    • Line 38 - a name - value pair within the options object that indicates the minimum number of uppercase alphabetic characters must be one.
    • Line 39 - a name - value pair within the options object that indicates the minimum number of numeric characters must be one.
    • Line 40 - a name - value pair within the options object that indicates the minimum number of symbol characters must be one.
    • Line 41 - ends the option object and isStrongPassword function.
    • Line 42 - the custom message if the password check fails.
    • Line 43 - ends the array containing all the checks.
    • Line 44 - ends the anonymous function opened on line 4.
  8. Carefully review the code for any warnings or errors.
  9. Save the file.

The Registration Check Function

The previous function declares the rules to be used to check the incoming registration data. Now, we will write the function to check the data against the rules. In addition, if errors are found, then the errors, along with the initial data, will be returned to the registration view for correction. This returning of data, so it doesn't have to be re-typed, is referred to as "Stickiness".

  1. In the same account-validation.js file, create a few empty lines below the previous function.
  2. Add the function as shown below:
/* ******************************
 * Check data and return errors or continue to registration
 * ***************************** */
validate.checkRegData = async (req, res, next) => {
  const { account_firstname, account_lastname, account_email } = req.body
  let errors = []
  errors = validationResult(req)
  if (!errors.isEmpty()) {
    let nav = await utilities.getNav()
    res.render("account/register", {
      errors,
      title: "Registration",
      nav,
      account_firstname,
      account_lastname,
      account_email,
    })
    return
  }
  next()
}

module.exports = validate
  1. An Explanation

    • Lines 1-3 - a multi-line comment introducing the function.
    • Line 4 - creates an asynchronous, anonymous function which accepts the request, response and next objects as parameters and assigns it to the "checkRegData" property of the validate object.
    • Line 5 - use JavaScript destructuring method to collect and store the firstname, lastname and email address values from the request body. Notice that the password is not stored. These variables will be used to re-populate the form if errors are found. Best practice is to make the client redo the password. So, the password value will NOT be returned.
    • Line 6 - creates an empty "errors" array.
    • Line 7 - calls the express-validator "validationResult" function and sends the request object (containing all the incoming data) as a parameter. All errors, if any, will be stored into the errors array.
    • Line 8 - checks the errors array to see if any errors exist. Notice the "!" which inverts the test, meaning errors IS NOT empty.
    • Line 9 - calls for the navigation bar to be queried and built.
    • Line 10 - calls the render function to rebuild the registration view.
    • Lines 11-17 - sends these items back to the view. Notice the errors array is returned, as are the values for the first and last names and email address.
    • Line 18 - the "return" command sends control of the process back to the application, so the view in the browser does not "hang".
    • Line 19 - ends the check for errors begun on line 8.
    • Line 20 - if no errors are detected, the "next()" function is called, which allows the process to continue into the controller for the registration to be carried out.
    • Line 21 - ends the function begun on line 4 of this code.
    • Line 22 - left intentionally blank.
    • Line 23 - exports the validate object for use elsewhere.
  2. Carefully review the code for any warnings or errors.
  3. Save the file.

Registration Route

With the validation rules and error handler function written, we can apply them to the registration process, and the place to do that is in the route. Remember that the data should be validated before being sent to the controller for processing.

  1. Find and open the accountRoute.js file in the routes folder.
  2. At the top of the file, add a require statement to bring the account-validation page, from the utilities folder into the routes scope. Like this:
    const regValidate = require('../utilities/account-validation')
  3. Locate the POST route that watches to process the incoming registration attempt.
  4. Alter the route by adding the rules function after the path, then add a comma and add the call to the checkRegData function. Make sure it is separated from the controller function by a comma. These can be placed on their own lines to enhance readability. When done, the route could look like this:
    // Process the registration data
    router.post(
      "/register",
      regValidate.registationRules(),
      regValidate.checkRegData,
      utilities.handleErrors(accountController.registerAccount)
    )
  5. An Explanation

    • Line 1 - A comment for the route
    • Line 2 - The router object using a "post" property.
    • Line 3 - The path being watched for in the route.
    • Line 4 - The function containing the rules to be used in the validation process.
    • Line 5 - The call to run the validation and handle the errors, if any.
    • Line 6 - The call to the controller to handle the registration, if no errors occur in the validation process.
    • Line 7 - ends the route.
  6. Carefully review the code for any warnings or errors.
  7. Save the file.

Registration View Error Display

With the validation in place, the registration view could be passed an array of errors (up to four total). Currently, the view is not equipped to display more than one. We need to alter to code so that all errors could be displayed.

  1. Find and open the registration view in the views > account folder.
  2. Add an EJS code block, below where messages are displayed, to display errors, like the example below:
    <% if (errors) { %>
      <ul class="notice">
     <% errors.array().forEach(error => { %>
       <li><%= error.msg %></li>
    <%  }) %>
     </ul>
    <% } %>
  3. An Explanation

  4. Line 1 - an EJS code block containing an "if" control structure to detect if an error variable exists and is not null.
  5. Line 2 - If there are errors, create an unordered list with a class for styling.
  6. Line 3 - Treat the errors variable as an array and loop through the array, with each individual error being sent into a callback function.
  7. Line 4 - create a list item and display the message attached to the error element.
  8. Line 5 - closes the callback function and forEach loop.
  9. Line 6 - closes the unordered list.
  10. Line 7 - closes the if control structure.
  11. Carefully review the code for any warnings or errors.
  12. Save the file.

With the addition of the errors display code block, we just broke the page. This is because when the controller is called to deliver the view initially, there is no errors array. This will cause an error. Let's fix this before moving forward.

  1. Open the accountController file.
  2. Locate the buildRegister function.
  3. Add the following line to the data object being sent to the view within the res.render() function.
  4. errors: null,
  5. Save the file.

A Recap

Time to Test

Testing this code can be tricky because we have already implemented the client-side validation. The easiest way is to use the novalidate! bookmarklet (installed into your browser in a separate activity this week). Fill out the form incorrectly or not completely, then click the novalidate! tool to turn off the client-side validation. Then, submit the form to the controller. See what happens. I demonstrate this in the video below.

  1. Ensure that all files are saved.
  2. Start the local server using "pnpm run dev" in a VSC terminal.
  3. In your browser, open the project using "localhost:5500".
  4. Navigate to the registration view using the "My Account" link and the registration link in the login view.
  5. Click the "noValidate" bookmarklet to disable all client-side checks.
  6. Attempt to submit the form with all fields empty. The errors should be displayed for all four inputs.
  7. If this worked, continue to attempt different types of errors to test the ability of your checks to catch the errors.
  8. Note that the inputs will be empty when errors are returned because we have not yet implemented stickiness into the form.
  9. If this worked, then stand up and do a dance. If not, troubleshoot, talk to your learning team members, get help, but get it working.
  10. Do not move to the next activity until this process is functioning.
  11. When done, be sure to shut down the server in a VSC terminal, with "Control + C".

Successful Registration

Once the testing is complete and the checks are working correctly, when correct data is submitted, a new client should be registered. You can confirm this by checking the database.