Learn to understand authentication and authorization and how to add it into your app
Contents
- Authentication vs Authorization
- Validation
- JOI package
- Lodash package
- Hashing
- What is hashing?
- What is a salt?
- Bcrypt package
- Generating a JSON Web Token
- What are JSON Web Tokens?
- JSON Web Tokens with Mongoose
Authentication vs Authorization
- Authentication is the process of determining if the user is who he/she claims to be. It involves validating their email/password.
- Authorization is the process of determining if the user has permission to perform a given operation.
Validation
- We use validation to verify that credentials are correct, and the user is who they say they are.
JOI Package
- We can download the JOI package to describe your data appropriately and validate that your schemas are appropriate.
npm i joi
- When using Joi, we typically make a duplicate of our schema using Joi.object()
- Here's an example of a typical code structure using Joi:
const Joi = require("joi");
const userSchema = new mongoose.Schema({ //Your Schema
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
minlength: 5,
maxlength: 1024,
}
});
const schema = Joi.object({ //Joi Schema for validation
name: Joi.string().required(),
email: Joi.string().required().email(),
password: Joi.string().min(5).max(255).required(),
});
exports.User = User;
exports.userSchema = schema;
- After making our Joi schema, we can validate using schema.validate() where schema is your schema name.
const { User, userSchema } = require("../models/user");
router.post("/", async (req, res) => {
const { error } = userSchema.validate(req.body); //Joi validation
if (error) return res.status(400).send(error.details[0].message)
});
//Returns an error if userSchema is not validated
- To add password complexity, we can use joi-password-complexity package which gives options like what characters are required for passwords.
You can find out more about joi documentation here: joi.dev - 17.6.1 API Reference
Lodash Package
- Lodash is a JavaScript library that works based on underscore.js and provides elegance for code through simplifying arrays and objects.
npm i lodash
- Typically, our routers have code like this, but we can refactor this better with lodash:
router.post("/", async (req, res) => {
...
res.send({
name: req.body.name,
email: req.body.email,
})
});
- In order to refactor, the convention is to use an underscore as the imported module. Afterwards, you call the pick() method and the object model (the user in this case) you're extracting from, followed by the array of properties you want to grab. Notice how this code looks a lot more elegant especially if we are picking many properties from a user model.
const _ = require("lodash");
router.post("/", async (req, res) => {
...
res.send(_.pick(user, ["_id", "name", "email"]))
});
You can learn more about Lodash in their documentation: Lodash Documentation
Hashing
What is Hashing?
- Hashing refers to creating substitute information for code by mapping characters to new values.
password = '1234'
// If we hash this, the password can show something like 'abcd' and we would have to decode this later to get our original data
- While hashing is great way of hiding data, hackers can still get access to sensitive information like passwords since they can simply compile a list of popular passwords and reverse engineer the patterns in order to find your password. That's where salt comes in.
What are salts?
-We use salts in order to securely hash passwords. Salts are random strings that are generated in the beginning or end of your data string, and this makes it really hard for hackers to decode.
Bcrypt Package
-To securely hash and add salt to passwords, we can use a package called bcrypt:
npm i bcrypt
-After importing the module, you can generate a salt with bcrypt by using the method genSalt() where the parameter is the cost factor for hashing. The higher the number, the more time the algorithm takes and the more difficult it is to reverse a hash using brute force:
// Hashing passwords
const salt = await bcrypt.genSalt(10); //abcd
const hashed = await bcrypt.hash('1234', salt); //abcdefgh
- To validate a password using bcrypt, we have to compare our password with the hashed function which will extract out the salt string and confirm if the password matches with the hashed password.
// Validating passwords
const isValid = await bcrypt.compare('1234', hashed);
Generating JSON Web Tokens
What are JSON Web Tokens?
- A JSON Web Token (JWT) is a JSON object encoded as a long string. We use them to identify users. It’s similar to a passport or driver’s license. It includes a few public properties about a user in its payload. These properties cannot be tampered because doing so requires re-generating the digital signature.
-When the user logs in, we generate a JWT on the server and return it to the client. We store this token on the client and send it to the server every time we need to call an API endpoint that is only accessible to authenticated users.
To debug your jwt tokens you can use the documentation here: https://jwt.io/
- To generate JSON Web Tokens in an Express app use jsonwebtoken package.
npm i jsonwebtoken
- Afterwards, you can use sign() to add a signature to the jwt token which will provide the credentials for authorization. You can add any kind of signature as the privateKeyName but make sure to hide this as a secret environment variable to prevent hackers from pretending to be you.
// Generating a JWT
const jwt = require('jsonwebtoken');
const token = jwt.sign({ _id: user._id}, 'privateKeyName');
- You'll want to also send the token in your header so that you can make API requests. Set a new header as 'x-auth-token', followed by the token you created.
res.header("x-auth-token", token)
Json Web Tokens with Mongoose
-It's also a best practice to encapsulate logic in your Mongoose models. Models can have methods added to them, so for your user model , you can add the method for generating an authtoken.
// Adding a method to a Mongoose User model
userSchema.methods.generateAuthToken = function () {
const token = jwt.sign({ _id: user._id}, 'privateKeyName')
return token;
};
// We can reuse the token by adding it to our router logic
router.post("/", async (req, res) => {
...
const token = user.generateAuthToken();
//This creates a webtoken which uses the userID as a form of identification
res.send(token);
});
- It's also a best practice to Implement authorization using a middleware function. Example code below:
const jwt = require("jsonwebtoken");
function auth(req, res, next) {
const token = req.header("x-auth-token");
if (!token) return res.status(400).send("Access Denied. No token provided.");
// This looks for the token in the header with the name x-auth-token and if its not found
// it returns access denied for anytime a user wants to handle a request
try {
// This decodes the token and returns the payload and the next() moves on to the route handler
const decoded = jwt.verify(token, process.env.jwtPrivateKey);
req.user = decoded;
next();
} catch (ex) {
res.status(400).send("Invalid token");
}
}
module.exports = auth;
- We can use this middleware in our routes by placing "auth" before the route handler and after the endpoint. For example, take a look at the route below where a user can access their own user data through the '/me' endpoint and checking if they are authorized using the auth middleware.
router.get("/me", auth, async (req, res) => {
const user = await User.findById(req.user._id).select("-password");
res.send(user);
});
"select("-password") prevents the database from showing password"
- Return a 401 error (unauthorized) if the client doesn’t send a valid token.
- Return 403 (forbidden) if the user provided a valid token but is not allowed to perform the given operation."
- You don’t need to implement logging out on the server. Implement it on the client by simply removing the JWT from the client.
- Do not store a JWT in plain text in a database. This is similar to storing users’ passports or driver's license in a room. Anyone who has access to that room can steal these passports. Store JWTs on the client. If you have a strong reason for storing them on the server, make sure to encrypt them before storing them in a database.
Comments