Adding Two-factor Authentication to an Express App
Two-factor authentication is a way of verifying the identity of a user multiple ways while they attempt to login to your application. Common ways for accomplishing this are by sending a text message, an email, or an application such as Google Authenticator. For this example, we're going to fake sending an email to the account, but really just log it to our console.
You can see the codebase for this demonstration on Github.
User login systems in Express are accomplished through the usage of middlewares. These middlewares pull data from your HTTP request, such as your cookies or headers, and associate it to a user account. For a basic user login system, we will assume we are using cookie-based authentication.
Let us say we have three routes in our application:
/login
; a route to input a username and password. This route is accessible no matter what your login state is./verify
; a route to input a 2-factor authentication code. This route is accessible when your login state isPasswordAuthenticated
./account
; a route to actually see and update your user data. This route is accessible when your login state isTwoFactorAuthenticated
.
The first route would hold a form that will require a username and password for an account. If there is a match, the server would perform the following combination:
- The server would add an entry to a database that states there is currently a session for that user. This entry would store data something like:
{ sessionId: "a926d5c2-2fb8-4524-9e15-a05d50391fb0", userId: "cd596677-7a88-45f4-86bc-800f19f0758c", loginState: "PasswordAuthenticated"}
. - The server would send a cookie down referencing the current user's session id.
On each request, we would have a middleware that would parse the user's cookies, and if a session id is provided, it would pull the session information and associate it to the current request.
On routes such as /account
, you would apply a middleware that checks for the user's current session. In the event that no session is active, it would redirect to /login
. In the event that a session is active but has a loginState
of PasswordAuthenticated
, it would redirect to /verify
.
When the user lands on /verify
, your server would generate a random code and send it to your users two-factor strategy (text, email, app). Let us say that your two-factor code is 678201
. At this point, you would update the session entry for that user and add an encrypted value for TwoFactorCode
, such as a bcrypted hash (in our case, $2a$04$Fn05pIC9DbGsPhQfLWVQj.5EDrd8mgp0IRvvl6mhe3sqfD5ArIgpm
).
The /verify
page of your application would have a form for the user to submit the code that was sent to them, and on submission it would verify that code against the one stored in the session object. If it is a match, then the user's session state would be updated to have a loginState
of TwoFactorAuthenticated
.
When the user lands on routes such as /account
, the first middleware that checks the login state would be applied. If that middleware allows the request to continue, a middleware would be applied that validates a user's session state is TwoFactorAuthenticated
. If it is not, the user would be redirected to the /verify
page.
There are other points you would want to add two-factor verification to your application, such as when your user is attempting to change their personal information. In order to accomplish this, when you load up that route you would reset the user session state to PasswordAuthenticated
, and simply store a redirectUrl
that the user is redirected to upon successful verification.