Multiplayer UI - Two factor authentication login (Part 2)
ui tutorials
8/6/2024
Kaloyan Geshev
Enabling two-factor authentication adds an extra layer of security, ensuring that only the rightful account owner can access it. This is typically achieved by generating a QR code that links the user’s account with an authenticator app like Google Authenticator. The app then generates a code required for logging into the account. The server processes this code to verify the login attempt.
In this tutorial, we will use otplib implementing two-factor authentication and qrcode to generate the QR code needed for linking the user’s account with the Google Authenticator app.
Showcase Overview
We will extend the express server created for the Multiplayer UI in this post by adding two-factor authentication during user login. Additionally, we will include a new UI page for handling this authentication and displaying the QR code for the user to scan with the authenticator app.
Source location
You can find the complete sample source within the ${Gameface package}/Samples/uiresources/UITutorials/MultiplayerUI directory.
src folder contains the UI source code.
api folder contains the Express server source code.
Ensure to run npm i before testing the sample.
By default, two-factor authentication is disabled. To test it, run the express server locally by either:
Executing npm run start:server:2fa to enable login authentication with 2FA.
Adding TWO_FACTOR_AUTHENTICATION_ENABLED="true" to the .env file and then starting the server locally with the npm run server:start command.
Refer to the README.md file in this directory for information on running the sample locally or previewing it without building or starting any processes.
Getting started - Backend
We will begin by modifying the backend to enable 2FA in the login method and adding additional APIs for handling code verification and linking to Google Authenticator.
First, install the required modules:
Add additional fields to the user
To store information about two-factor authentication in the user account, we need to extend the schema of the user in our database. We will add two new fields:
The totpSecret field stores the generated secret needed for user authentication via the Authenticator app, while twoFactorEnabled indicates if the user’s account is linked with the app.
Enable 2FA check in the login method
To enable 2FA during user login, we need to update our login method.
By default, 2FA is disabled during user login. This behavior is controlled via the TWO_FACTOR_AUTHENTICATION_ENABLED environment variable. To enable environment variables on the server, we previously installed the dotenv module and need to import it in the index.js file.
Without the TWO_FACTOR_AUTHENTICATION_ENABLED variable, 2FA will remain disabled.
When 2FA is enabled, we need to handle user login properly by checking if the user has already linked the Authenticator app. We determine this by checking if the user has a totpSecret in the database - if (!user.totpSecret).
If the user has a secret stored in the database, we return an error indicating that TOTP verification is required. The user should enter the generated code from the Authenticator app into the UI.
If the user does not have a secret stored in the database, the account should be linked with the Authenticator app. We generate a new secret and a QR code for adding the secret to the app by scanning it. The generated data is then sent to the UI.
Entries for linking the app or verifying the code.
When the user needs to authenticate using the Authenticator app, there are two scenarios: the user has either already linked the app to their account or not. To handle these scenarios, we will create two new routes:
For that reason we will create two new entry points to the user routes:
bindTotp method
To link the Authenticator app with the user account, we need to validate the generated code from the app and then store the secret in the user’s account if the code is correct. After this, the user can be logged in.
totpVerify method
If the user has already linked the app to their account, we only need to check the generated code from the Authenticator app. If the code is correct, the user can successfully log in.
Getting started - Frontend
To implement two-factor authentication (2FA) in the frontend, we’ll create a dedicated page where users can either scan a QR code to link their account with an authenticator app or input the code generated by the app.
Refactoring the login page
First, we need to refactor the login page to reuse some of its logic on the 2FA page. We’ll create a custom hook for login operations that returns the states for the welcome message, user name, and a function to handle post-login operations.
This hook manages the state for userName, the zoom-out animation, and the visibility of the welcome message. It also clears the timeout if the component is unmounted by using the useEffect hook.
Next, integrate this hook into the login page.
Navigate to the 2FA page
When the user tries to log in and the server requires two-factor authentication, we need to navigate to the 2FA page. We’ll create a handler to check if 2FA is needed and navigate accordingly.
2FA page
We will create a new page to enable users to perform two-factor authentication (2FA).
First, we need to add this page to the router as a public path.
Next, we will create a new page that reuses the login logic but ensures the user completes 2FA using their phone app.
Retreiving navigation state
This page receives state from the navigation API when the login page redirects here. We use this state to determine whether to show the QR code for linking the app or just the verification code input.
If the server generates a secret when the user tries to log in, we display it as a QR code.
Otherwise, only the verification code input is displayed.
Handle bad navigation to the page
We handle scenarios where the user is already logged in or the error state from navigation is undefined or unrelated to 2FA.
Performing 2FA
To perform 2FA, we submit the verification code in the onSubmit method.
We also reuse the login logic wrapped in the useLogin hook.
We send a request to the server via verifyIdentityRequest and check if the verification succeeded with hasUserVerified. If the user is verified, we call loginUser.
Verifying identity
There are two types of requests to the server from verifyIdentityRequest. One for linking the app if it hasn’t been done yet, and another for verifying the generated code if the app is already linked.
For linking the app, we send a POST request to /api/bind-totp with the generated code and the secret.
For verification, we send a POST request to /api/verify-totp with the code.
By refactoring the login logic and handling 2FA requirements, we ensure a smooth user experience when integrating two-factor authentication into our application.