DEV Community

Cover image for Validation, Authentication and Authorization
Michael Otu
Michael Otu

Posted on

Validation, Authentication and Authorization

Content

  • Validation
  • Authentication
  • Authorization
  • Adding validation
  • Adding Authentication and Authorization

In the previous excerpt, Basic CRUD API with express, we discussed creating a CRUD API with express. We installed extensions for our development environment. We also installed express, an npm package, to create our server. We discussed how to create POST, GET UPDATE and DELETE routes, and how to parse (access) data from the request body, query string and request parameter.

This excerpt will discuss validation, authentication and authorization. We will be building on top of the previous excerpt. Refer to the previous material for the code snippet. You can also reference this material from here.

We are using the snippet from the previous excerpt. You can follow that to have the starter code.
Since we always start on a fresh environment, I will have to follow the process of creating a node app and copy the content of the index.js file and package.json. Remember to run npm i or yarn to install the npm packages (if you cloned the project)
If you are having any issues, the project can be found here on GitHub, expense-tracker-api-articles/01-basic-crud-api-with-express/expense-tracker-simple-api or drop a comment. I or any capable individual may attempt to help you, as they see fit.

Validation

Before we jump into definitions of what is what and all that, let me draw your attention to why we have to validate data received from users by referencing some articles written on DEV. Check out these articles to have a different voice on why validations is importance. I think it will spare us the definition of validation.

From these very short portions about validations from these articles, it should be clear why validation is important and why it should matter. They all point out that it will be a security disaster if proper validations aren't done and this obviously will/may lead to a loss, a financial loss, primarily. One's business may lose data, users, integrity, money, etc as a result of a security breach. As such I, and the community as a whole, encourage you to validate data before inserting or updating existing records.

There are several instances where some client (app) communicates with the backend and these moments are mostly submission of forms.

In the previous excerpt where we were creating and updating expenses, what should we "consider" as an appropriate or acceptable value for name, amount and date?

  • In this case, the name should specify what object, element or event the expense was for. It could be a word or phrase that describes the expense.
  • For the amount, usually, the question should be, do we consider a negative value or even zero in this case as an acceptable value for the amount? What is the limit of the value that the user can pass? This is in a sense that, can a user pass 1 million or 1 billion?
  • For the date value, we should consider the format of the expected expense date. We can have it that the date should start with the year followed by the month and then the day. The year, the month and the day can be separated by some specified character. We used a hyphen, -. So here you can specify whether the date should be separated by a forward slash, /, or even a colon, :. You decide.

If you have been following this series, then you know that we have written some validations in the past. From, JavaScript Essentials: Part 6 (Mastermind in Javascript), this is an example of a validation:

function isValidRound(rounds) {
    return MIN_ROUNDS <= rounds && rounds <= MAX_ROUNDS && rounds % 2 == ZERO;
}
Enter fullscreen mode Exit fullscreen mode

Let's define some validation rules:

  • Name
    • Must be a string
    • Must not be empty or null (it is required)
    • Minimum characters of 10
    • Maximum characters of 255
  • Amount
    • Must be a number or numeric string
    • Must not be empty (it is required)
    • Must be a positive number
  • Date
    • Must be a string
    • Could be empty or null (it is an option, current date will be used)
    • Must be in ISO format

As an exercise, implement three functions that take in the specified data, validate it and return a boolean.

function isValidName(name) {
    // returns a bool
}

function isValidAmount(amount) {
    // returns a bool
}

function isValidDate(date) {
    // returns a bool
}
Enter fullscreen mode Exit fullscreen mode

It will be cool to have them exported from another file called validation or some awesome name that hints at what these functions are doing.
It is also a good practice to separate concerns. If this sounds weird or strange then we can simply say that it is better to reduce the number of code in the index.js file.

At this point even if we don't define what validation is, with what we have discussed so far, one can have a solid idea of what validation is and its importance.

So, what is validation? Care to share with us what you believe it is, at this point?

Authentication

Earlier on with discussed that validation is a mechanism that ensures that the values or input taken from a user is an appropriate or an acceptable value that conforms to certain requirements or standards. The idea of authentication in some way is similar to validation.

In authentication, where we make sure that the claims of a user, passing their credentials to have access to some account, is legitimate. This means that we check or verify that a user is who they say they are, based on the credentials they pass. Mostly, these credentials are made up of a public identifier which could be an email, username or some string token (an alias which can be shared with others), and a secret which is dubbed as a password (πŸ™ˆ not safe to share). We first check if there exists a user with such an identifier, we then cross-check the password or the secret passed alongside the identifier against the password or the secret of the record obtained concerning the identifier. So in authentication, the identity of a user is verified.

Authorization

Authorization on the other hand has to do with what the user can and cannot do on a platform. So a user that claims to be John "cannot" assess a record that doesn't belong to John. John can not create, read, update or delete expense records that belong to Mark. John is not authorized access nor modify Mark's records. However, John "can" create, read, update and delete expense records that belong to John. Another form that authorization can appear is that you have an admin and these admin may have some sort of access levels or permissions. This access level determines what the admin can and cannot do on the platform. Usually, an admin that is a moderator, can only flag and report some accounts and articles, or downvote them (during moderation). Surely there is this admin with enough privileges to even block an active user and even delete or hide an article or a comment.

Adding validation

I implemented my validations for the name, amount and date. Some comments explain some decisions made.

// file name: validations.js

// typeof operator returns the type of a value

// check that a value is a string
const isString = (arg) => typeof arg === "string";

// check that a value is a number
// const isNumber = (arg) => typeof arg === "number";
// if we use this approach then, "2.99" fails as a number even though it is a numeric string
// as such we are going with this and later casting the values to numbers
const isNumber = (arg) => !isNaN(Number(arg));
// when arg is non-numeric or a numeric string, Number(arg) returns NaN
// isNaN checks if a value is NaN and returns true if so else false
// which is why we negate the results from isNaN(...)

/**
 *
 * @param {*} name
 * -   Must be a string
 * -   Must not be empty or null (it is required)
 * -   minimum characters of 10
 * -   maximum characters of 255
 * @returns boolean
 */
function isValidName(name) {
    if (!name || !isString(name)) {
        return false;
    }

    return name.length >= 10 && name.length <= 255;
}

/**
 *
 * @param {*} amount
 * -   Must be a number or numeric string
 * -   Must not be empty (it is required)
 * -   Must be a positive number
 * @returns boolean
 */
function isValidAmount(amount) {
    if (!amount || !isNumber(amount)) {
        return false;
    }

    return amount > 0;
}

/**
 *
 * @param {*} date
 * -   Must be a string
 * -   Could be empty or null (it is an option, the current date will be used)
 * -   Must be in YYYY-MM-DD format
 * @returns boolean
 */
function isValidDate(date) {
    if (!date || !isString(date)) {
        return false;
    }

    const parsedDate = date.split("-");

    if (parsedDate.length !== 3) {
        return false;
    }

    const [year, month, day] = parsedDate;

    // there are more validations to be done on the years, month and day
    // there are 12 months, the max day is 31 and others
    return isNumber(year) && isNumber(month) && isNumber(day);
}

module.exports = { isValidName, isValidAmount, isValidDate };
Enter fullscreen mode Exit fullscreen mode

Adding Authentication and Authorization

For the sake of demonstration, we will create a user record for the authentication and since we have not done database integration yet, we will use an array of objects instead (we did the same for expenditures). Now, let's do some cleanup. We are going to move the expenditures records (array) into a file and export it. Since it is acting as our data source, maybe we can name the file, data.js. After that, we'd add another variable for users.

Make sure that the expenditures and users are exported and imported appropriately.

We will add these lines to data.js

let expenditures = [
 ...
]

let users = [
 {
        id: "c96b08b8-f92f-4e27-8f04-c4f3604907d6",
        email: "john@gmail.com",
        password: "John123$",
 },
];

module.exports = { expenditures, users };
Enter fullscreen mode Exit fullscreen mode

And import expenditures and users in index.js

...
// data source imports
const { expenditures, users } = require("./data");
...
Enter fullscreen mode Exit fullscreen mode

"...", the three dots indicate that there is some code there. Perhaps the original

We will implement an endpoint to create a user and another, to log in users. There are a few things we have to consider:

  • The data that we are using is saved in memory. This means that when the server is restarted, the whole data will be lost.
  • For the User ID, we are returning or using a UUID.
  • The password will be stored as the raw password (NOT BEST PRACTICE, JUST FOR DEMO)

Sign up

To create a user, we first have to check the user data (array) and make sure that the email that the user is providing does not exist - there is no element in the array whose email matches the provided email. When there is no user with such an email, we then hash the password (for now just the raw password) and generate a UUID to uniquely identify the user.

These steps should help with the signup process:

  • In expense-tracker-api.http, add a post request for user signup
### Sign up
POST http://localhost:3000/users/signup
Content-Type: application/json

{
"email": "johndoe@gmail.com",
"password": "JohnPwd12_"
}
Enter fullscreen mode Exit fullscreen mode
  • create a post request to handle user signup requests. The route should match the one above, expecting email and password from the request body
  • validate the email and password
  • check the users array (from the data file) that there is no record with an email that matches the email provided by the user. If such a record exists, return an appropriate response
  • we are supposed to hash the password but for demo reasons, we'd pass or save the raw password as is
  • generate a UUID for the new user
  • save the email, password and UUID by updating the users array
  • as part of the successful response, add the email and UUID and a message that says sign up successful or whatever appropriate message
  • it will be nice to log the new user record so that you can copy and manually add it to the user data (just to make sure that you have something static you are working with)
// sign up
app.post("/users/signup", (req, res) => {
    const { email, password } = req.body;

    if (!isValidEmail(email)) {
        return res.status(200).json({
            success: false,
            message: "Invalid email",
        });
    }

    if (!isValidPassword(password)) {
        return res.status(200).json({
            success: false,
            message: "Invalid password",
        });
    }

    // making sure that there is no record with the same email as this user's
    const existingUsers = users.filter((user) => user.email === email);

    if (existingUsers.length > 0) {
        return res.status(200).json({
            success: false,
            message: "Email already taken",
        });
    }

    // we are supposed to do password hashing here (recommended practice)

    // generate uuid unique to this user
    const uuid = crypto.randomUUID();

    // save this record
    const newUser = {
        id: uuid,
        email,
        password,
    };

    console.log(newUser);

    users = [...users, newUser];

    return res.status(200).json({
        success: true,
        message: "Sign up successful",
        data: {
            id: uuid,
            email,
        },
    });
});
Enter fullscreen mode Exit fullscreen mode

This is the response that should be expected assuming we are on the same page.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 128
ETag: W/"80-1wtZS+F4SzQ3LG+Q5EXmhzMEFaA"
Date: Wed, 15 Jan 2025 12:00:26 GMT
Connection: close

{
  "success": true,
  "message": "Sign up successful",
  "data": {
    "id": "decec694-6d97-4199-8544-f4bbe7ea753b",
    "email": "johndoe@gmail.com"
 }
}
Enter fullscreen mode Exit fullscreen mode

And the console log should look similar to

{
  id: 'decec694-6d97-4199-8544-f4bbe7ea753b',
  email: 'johndoe@gmail.com',
  password: 'JohnPwd12_'
}
Enter fullscreen mode Exit fullscreen mode

Did you get any other response where the success was false? What was it and how did you fix it?

Log in

To implement login, will check that there is a user with the provided email and that the User password matches the password (hash) of the retrieved record. The login will be very similar to the signup.

Try implementing the login.

When a user logs in to our platform, we have to find a way to allow the user to browse through the resources available to them without them having to log in all the time. What we are going to look at is how to authorize a user to CRUD their data (i.e. expenses and user data). Usually, we provide the user a means for them to access their data without having to log in all the time. There are several to do this. Some of them are:

  • Basic authorization: we send the user's credentials in every request they make, i.e. their email in this case with the password. We would encode (base64) these credentials for security's sake.
  • API key (Or token): this is usually a string issued by the platform. It can be used to uniquely identify each user.
  • Bearer token: an example of a bearer token is JSON Web tokens.
  • etc

There are several complicated authorization schemas out there. However, at this point, we will discuss each of these above forms of authorization.

  • Basic auth: The downside with this approach is that the user has to pass these encoded credentials all the time in every request. It is practically logging in all the time. And the user's account can be compromised easily as well. The only way to resolve this is to ... (no I am not talking about this... too much work - don't do this, I hope we understand). Use any of the ones mentioned below.
  • API key: Just like the basic auth, api keys can be compromised. The advantage of an api key is that we can generate another api when it is compromised. We can have it that the user has to request new api keys every week or within some time frame. We set up a mechanism to expire api keys so that the user will have to generate a new api key. Another downside of this is that we have to save the api key somewhere so that we can match it against any request. (Use a cache)
  • Bearer token (jwt): jwt is like an api token but the token itself can be time-bound. The token can expire and as such will become invalid after that period of expiry onwards. The token can store a payload which when parsed would be useful for identifying the owner of the token.

In most cases after login, an authorisation token is added as part of the response to the user. This token is then used by the user in every request thereafter to access the api resources. So Just to make this simple and to demonstrate how we can pass an api key for authorization, let's base64 encode the email and uuid passed as the auth token.

It is simple to encode a string to base64. Do, Buffer.from(STRING TO ENCODE).toString("base64"). So for our case, we want to use, email:uuid as the string to encode.

So before the return statement, let's add the encoding and update the response to include the auth token.

// sign up
app.post("/users/signup", (req, res) => {
    // some codes here were redacted

    const token = Buffer.from(`${email}:${uuid}`).toString("base64"); // newly added

    return res.status(200).json({
        success: true,
        message: "Sign up successful",
        data: {
            id: uuid,
            email,
            token, // newly added
        },
    });
});
Enter fullscreen mode Exit fullscreen mode

Run the signup request again with another email and compare the difference in the response. Anything different? What was it?

Now we can do a proper login. The login request will not be that different from the sign-up. It is just the route that will be different.

// login
app.post("/users/login", (req, res) => {
    const { email, password } = req.body;

    if (!isValidEmail(email)) {
        return res.status(200).json({
            success: false,
            message: "Invalid email",
        });
    }

    if (!isValidPassword(password)) {
        return res.status(200).json({
            success: false,
            message: "Invalid password",
        });
    }

    // find a user with the same email and password: authentication taking place here
    const authUser = users.filter(
        (user) => user.email === email && user.password === password
    );

    // the number of records expected is one, anything else is invalid
    if (authUser.length !== 1) {
        return res.status(200).json({
            success: false,
            message: "Invalid credentials",
        });
    }

    // we are supposed to compare the password with the hash normally
    const user = authUser[0];

    const token = Buffer.from(`${user.email}:${user.id}`).toString("base64");

    return res.status(200).json({
        success: true,
        message: "Login successful",
        data: {
            id: user.id,
            email: user.email,
            token,
        },
    });
});
Enter fullscreen mode Exit fullscreen mode

This is the login request:


### Log in
POST http://localhost:3000/users/login
Content-Type: application/json

{
 "email": "johndoe@gmail.com",
 "password": "JohnPwd12_"
}
Enter fullscreen mode Exit fullscreen mode

And this is a sample response on my end.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 216
ETag: W/"d8-aHIA5gYn4IKu8QQsFeyfBgNuK/I"
Date: Wed, 15 Jan 2025 18:20:17 GMT
Connection: close

{
  "success": true,
  "message": "Sign up successful",
  "data": {
    "id": "5991be49-d396-4f3e-ae61-89f38207db6c",
    "email": "johndoe1@gmail.com",
    "token": "am9obmRvZTFAZ21haWwuY29tOjU5OTFiZTQ5LWQzOTYtNGYzZS1hZTYxLTg5ZjM4MjA3ZGI2Yw=="
 }
}
Enter fullscreen mode Exit fullscreen mode

Now we have an auth token. We will use the auth token to access resources only available to the owner of that token. To do that we have to modify and add (or update) the existing records to account for the userId. To make it simpler, I'd suggest we create multiple users and update several or add more expenses with userIds.

I have two users and three expenses (statically) and as such I will update them as is:

// data.js
// dummy data
let expenditures = [
    {
        id: "0fb7f7b7-9ed4-41af-858c-840258788479",
        name: "Legion Tower 7i Gen 8 (Intel) Gaming Desktop",
        userId: "c96b08b8-f92f-4e27-8f04-c4f3604907d6",
        amount: 2099.99,
        date: "2024-12-31",
    },
    {
        id: "2ea3786a-7391-46e8-a130-4de79504bd29",
        userId: "c96b08b8-f92f-4e27-8f04-c4f3604907d6",
        name: "Apple MacBook Pro 16-inch",
        amount: 2499.99,
        date: "2024-12-15",
    },
    {
        id: "a29e3057-c07d-4009-be19-f8da705bd5b8",
        userId: "decec694-6d97-4199-8544-f4bbe7ea753b",
        name: "Samsung Galaxy S24 Ultra",
        amount: 1199.99,
        date: "2024-12-10",
    },
];

let users = [
    {
        id: "c96b08b8-f92f-4e27-8f04-c4f3604907d6",
        email: "john@gmail.com",
        password: "John123$",
    },
    {
        id: "decec694-6d97-4199-8544-f4bbe7ea753b",
        email: "johndoe@gmail.com",
        password: "JohnPwd12_",
    },
];

module.exports = { expenditures, users };
Enter fullscreen mode Exit fullscreen mode

As presented, now an expense also has an id (a uuid).

Protected endpoints

A protected endpoint or route requires some sort of "authorization" to access resources on that route. In this case, to protect an endpoint, we expect the request to have an auth token and that we can trace the auth token to an authentic user.

The auth token can be passed in any of the means, discussed so far, that data can be passed in the request: body, param, query and headers. We would pass the auth token in the headers.

Let's modify the List Expenditures endpoint to account for the auth token passed in the headers.

When the auth token is added, the request will look like this, below. Or rather, this is how we'd pass the auth token in the headers.

### List Expenditures
GET http://localhost:3000/expenditures
Authorization: am9obmRvZTFAZ21haWwuY29tOjU5OTFiZTQ5LWQzOTYtNGYzZS1hZTYxLTg5ZjM4MjA3ZGI2Yw==
Enter fullscreen mode Exit fullscreen mode

We can get and log the content of the headers by doing so.

app.get("/", (req, res) => {
    return res.send(req.headers);
});
Enter fullscreen mode Exit fullscreen mode

And we'd get something similar to

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 208
ETag: W/"d0-H4HndowWbSuUVoTjck9QS9+XNXo"
Date: Thu, 16 Jan 2025 08:27:35 GMT
Connection: close

{
  "user-agent": "vscode-restclient",
  "authorization": "am9obmRvZTFAZ21haWwuY29tOjU5OTFiZTQ5LWQzOTYtNGYzZS1hZTYxLTg5ZjM4MjA3ZGI2Yw==",
  "accept-encoding": "gzip, deflate",
  "host": "localhost:3000",
  "connection": "close"
}
Enter fullscreen mode Exit fullscreen mode

When we run

GET http://localhost:3000/
Authorization: am9obmRvZTFAZ21haWwuY29tOjU5OTFiZTQ5LWQzOTYtNGYzZS1hZTYxLTg5ZjM4MjA3ZGI2Yw==
Enter fullscreen mode Exit fullscreen mode

From the content in the headers, we can see a key-value pair of values and as present, we have "authorization": "am9obmRvZTFAZ21haWwuY29tOjU5OTFiZTQ5LWQzOTYtNGYzZS1hZTYxLTg5ZjM4MjA3ZGI2Yw==".

Great. At this point, I think we know what to do when we want to pass other values in the header.

authorization is the standard used when we have/want to pass auth tokens. We can use an arbitrary key and it should still work fine.

Now, we have to make sure the user making the request is authorized (using the auth token). We do that by decoding the auth token, in our case (because we encoded it), extracting the payload in there and comparing it against the users and if it's right, we can use the id (uuid) to fetch the expenses based on the userId.

When we were encoding, we did, Buffer.from(STRING TO ENCODE).toString("base64"). Now to decode is fairly similar. We'd do, Buffer.from(STRING TO DECODE, "base64").toString().

This is what we did to generate the auth token.

const token = Buffer.from(`${user.email}:${user.id}`).toString("base64");
Enter fullscreen mode Exit fullscreen mode

And we can decode it by

// decode the auth token
const credential = Buffer.from(authTokenFromHeader, "base64").toString();

// credentials will be EMAIL:UUID and we want just the uuid
// we can get both and user both to fetch a user
const [email, id] = credential.split(":");
// how is that?

console.log(email, id);
Enter fullscreen mode Exit fullscreen mode

When we put all these together, we will have the update endpoint to be:

// list expenditures
app.get("/expenditures", (req, res) => {
    // extra auth token from headers
    const authorization = req.headers.authorization;
    if (!authorization) {
        return res.status(200).json({
            success: false,
            message: "Unathorized, please login",
        });
    }

    // decode auth token
    const decodeToken = Buffer.from(authorization, "base64").toString();

    // parse the decoded token into the email and userId
    const [email, userId] = decodeToken.split(":");

    // now we can fetch the user with email and userId
    const isAuthenticUser = users.find(
        (user) => user.email === email && user.id === userId
    );

    if (!isAuthenticUser) {
        return res.status(200).json({
            success: false,
            message: "Unathorized, please login",
        });
    }

    // get the query string and check if it is not a number or something
    // that can be a number else set a default filter value of 0
    let amountMoreThan = Number(req.query.amountMoreThan);
    if (isNaN(amountMoreThan) || amountMoreThan < 0) {
        amountMoreThan = 0;
    }

    return res.json({
        success: true,
        data: expenditures.filter(
            (row) => row.userId === userId && row.amount > amountMoreThan
        ),
    });
});
Enter fullscreen mode Exit fullscreen mode

Log in and the request again.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 190
ETag: W/"be-LOBEpYsscV5twPwFE50Va2UM9d4"
Date: Thu, 16 Jan 2025 09:23:13 GMT
Connection: close

{
  "success": true,
  "data": [
 {
      "id": "a29e3057-c07d-4009-be19-f8da705bd5b8",
      "userId": "decec694-6d97-4199-8544-f4bbe7ea753b",
      "name": "Samsung Galaxy S24 Ultra",
      "amount": 1199.99,
      "date": "2024-12-10"
 }
 ]
}
Enter fullscreen mode Exit fullscreen mode

We are good for now. Now users can only access information that strictly belongs to them. How is that?

Conclusion

We have looked at how to do validations, authentication and authorization. What we have discussed here is roughly what these concepts are and some "rough" ways to implement them. For learning purposes, I believe these should be done.

A few things to consider:

  • Never save a raw password
  • Never use the auth mechanism employed here in the production environment i.e encoding the email and ID
  • There are cases where you you want to pass a different status code so that you can drop the success property in the json response. Yeah, do that. When you do that, instead of { data: [ ...😩 ]}, just return [ ...😩 ]

In the next excerpt, we are going to look at how to use a library to validate data and also hash passwords and break our application down.

Let me know what you think... πŸ₯³

Resources

Top comments (0)