The author of the article, the translation of which we are publishing today, says that now you can observe the growing popularity of authentication services such as Google Firebase Authentication, AWS Cognito and Auth0. The industry standard has become universal solutions like passport.js. But, given the current situation, it is commonplace that developers never fully understand what mechanisms are involved in the operation of authentication systems.
This material is devoted to the problem of organizing user authentication in the Node.js environment. In it, on a practical example, the organization of registration of users in the system and the organization of their entry into the system are considered. Issues such as working with JWT technology and user impersonation will be raised here.

')
Also, take a look at
this GitHub repository, which contains the code for the Node.js project, some examples of which are given in this article. You can use this repository as a basis for your own experiments.
Project Requirements
Here are the requirements for the project that we will deal with here:
- Having a database in which the user's email address and password will be stored, either - clientId and clientSecret, or - something like a combination of private and public keys.
- Use a strong and efficient cryptographic algorithm for password encryption.
The moment I write this material, I believe that the best of the existing cryptographic algorithms is Argon2. I ask you not to use simple cryptographic algorithms like SHA256, SHA512 or MD5.
In addition, I suggest you take a look at
this wonderful material, in which you can find details about the choice of the algorithm for password hashing.
Registration of users in the system
When a new user is created in the system, his password must be hashed and stored in the database. The password in the database is saved along with the email address and other information about the user (for example, there may be a user profile, time of registration, and so on).
import * as argon2 from 'argon2'; class AuthService { public async SignUp(email, password, name): Promise<any> { const passwordHashed = await argon2.hash(password); const userRecord = await UserModel.create({ password: passwordHashed, email, name, }); return {
User account details should look something like the one below.
User data obtained from MongoDB using Robo3TUser Login
Here is a diagram of actions performed when a user tries to log in.
User loginHere is what happens when a user logs in:
- The client sends the server a combination of a public identifier and a user's private key. Usually this is an email address and password.
- The server is looking for a user in the database by email address.
- If the user exists in the database - the server hashes the password sent to it and compares what happened with the password hash stored in the database.
- If the check is successful, the server generates a so-called token or authentication token - JSON Web Token (JWT).
JWT is a temporary key. The client must send this key to the server with each request to an authenticated endpoint.
import * as argon2 from 'argon2'; class AuthService { public async Login(email, password): Promise<any> { const userRecord = await UserModel.findOne({ email }); if (!userRecord) { throw new Error('User not found') } else { const correctPassword = await argon2.verify(userRecord.password, password); if (!correctPassword) { throw new Error('Incorrect password') return { user: { email: userRecord.email, name: userRecord.name, }, token: this.generateJWT(userRecord), }
Password verification is performed using the argon2 library. This is done to prevent the so-called "
time attacks ". When performing such an attack, the attacker tries to crack the password using brute force, based on an analysis of how long the server needs to generate a response.
Now let's talk about how to generate JWT.
What is JWT?
JSON Web Token (JWT) is a string-encoded JSON object. Tokens can be taken as a replacement for cookies, which has several advantages over them.
Token consists of three parts. These are header (header), payload (payload) and signature (signature). The following figure shows its appearance.
JwtThese tokens can be decoded on the client side without using a secret key or signature.
This can be useful for transferring, for example, metadata encoded within the token. Such metadata can describe the user's role, his profile, the duration of the token, and so on. They can be designed for use in front-end applications.
Here is what a decoded token might look like.
Decoded TokenGenerating JWT in Node.js
Let's create the
generateToken
function that we need to complete work on the user authentication service.
You can create JWT using the jsonwebtoken library. You can find this library in npm.
import * as jwt from 'jsonwebtoken' class AuthService { private generateToken(user) { const data = { _id: user._id, name: user.name, email: user.email }; const signature = 'MySuP3R_z3kr3t'; const expiration = '6h'; return jwt.sign({ data, }, signature, { expiresIn: expiration }); }
The most important thing here is coded data. Do not send secret information about users in tokens.
The signature (here - the
signature
constant) is the secret data that is used to generate the JWT. It is very important to ensure that the signature does not fall into the wrong hands. If the signature is compromised, the attacker will be able to generate tokens on behalf of users and steal their sessions.
Endpoint Protection and JWT Verification
Client code now needs to send JWT in each request to a secure endpoint.
It is recommended to include JWT in request headers. Usually they are included in the Authorization header.
Title AuthorizationNow, on the server, you need to create code that represents middleware for express routes. Put this code in the
isAuth.ts
file:
import * as jwt from 'express-jwt';
It is useful to be able to get complete user account information from the database and attach them to the request. In our case, this feature is implemented by middleware from the file
attachCurrentUser.ts
. Here is its simplified code:
export default (req, res, next) => { const decodedTokenData = req.tokenData; const userRecord = await UserModel.findOne({ _id: decodedTokenData._id }) req.currentUser = userRecord; if(!userRecord) { return res.status(401).end('User not found') } else { return next(); }
After implementing this mechanism, routes will be able to receive information about the user who makes the request:
import isAuth from '../middlewares/isAuth'; import attachCurrentUser from '../middlewares/attachCurrentUser'; import ItemsModel from '../models/items'; export default (app) => { app.get('/inventory/personal-items', isAuth, attachCurrentUser, (req, res) => { const user = req.currentUser; const userItems = await ItemsModel.find({ owner: user._id }); return res.json(userItems).status(200); })
Now the
inventory/personal-items
route is protected. To access it, the user must have a valid JWT. The route can also use user information to search the database for information it needs.
Why are tokens protected from intruders?
After reading about using JWT, you can ask the following question: “If the JWT data can be decoded on the client side, is it possible to process the token in such a way as to change the user ID or other data?”.
Token decoding is a very simple operation. However, it is impossible to “remake” this token without having that signature, that secret data that was used when signing JWT on the server.
That is why the protection of these sensitive data is so important.
Our server verifies the signature in the isAuth middleware. The express-jwt library is responsible for checking.
Now, after we figured out how the JWT technology works, let's talk about some interesting additional features that it gives us.
How to impersonate a user?
User impersonation is a technique used to log in to the system under the guise of some specific user without knowing his password.
This feature is very useful for super-administrators, developers, or support staff. Impersonation allows them to solve problems that appear only during the work of users with the system.
You can work with the application as a user without knowing his password. To do this, it is enough to generate JWT with the correct signature and with the necessary metadata describing the user.
Create an endpoint that can generate tokens for logging on to the system under the guise of specific users. This endpoint can only be used by a super-system administrator.
For the beginning, we need to assign to this user a role that is associated with a higher level of privileges than other users. This can be done in many different ways. For example, simply add the
role
field to the user information stored in the database.
It may look like the one below.
New field in user informationThe value of the super administrator
role
field is
super-admin
.
Next, you need to create a new middleware, which checks the role of the user:
export default (requiredRole) => { return (req, res, next) => { if(req.currentUser.role === requiredRole) { return next(); } else { return res.status(401).send('Action not allowed'); }
It must be placed after isAuth and attachCurrentUser. Now we will create an endpoint that generates JWT for the user on whose behalf the super-administrator wants to log in:
import isAuth from '../middlewares/isAuth'; import attachCurrentUser from '../middlewares/attachCurrentUser'; import roleRequired from '../middlwares/roleRequired'; import UserModel from '../models/user'; export default (app) => { app.post('/auth/signin-as-user', isAuth, attachCurrentUser, roleRequired('super-admin'), (req, res) => { const userEmail = req.body.email; const userRecord = await UserModel.findOne({ email: userEmail }); if(!userRecord) { return res.status(404).send('User not found'); return res.json({ user: { email: userRecord.email, name: userRecord.name }, jwt: this.generateToken(userRecord) }) .status(200); })
As you can see, there is nothing mysterious. The super administrator knows the email address of the user on whose behalf you want to log in. The logic of the above code is very similar to how the code works, which provides access for ordinary users to the system. The main difference is that the password is not verified.
The password is not verified here because it is simply not needed here. Endpoint security is provided by middleware.
Results
There is nothing wrong with relying on third-party services and authentication libraries. This helps developers save time. But they also need to be aware of the principles on which the operation of authentication systems is based, that they ensure the functioning of such systems.
In this article, we investigated the possibilities of JWT authentication, talked about the importance of choosing a good cryptographic algorithm for password hashing. We considered the creation of a mechanism for impersonating users.
Doing the same thing with something like passport.js is far from easy. Authentication is a huge topic. Perhaps we will come back to it.
Dear readers! How do you create authentication systems for your Node.js projects?