TOTP Generator Utility
Registered members can download the FREE Get Started Apps. I initially implemented MySQL for this site and the Get Started App. The Get Started App is the project I used to compose articles about setting up VS Code and developing Node with Express and the Embedded JavaScript (EJS) view engine. I decided to migrate this site to PostgreSQL. Rather than replacing the MySQL Get Started App, I decided to add a PostgreSQL Get Started App. Both apps will run without a database but can integrate with either MySQL or PostgreSQL by configuring environment variables in the .env file.
You can test two-factor authentication methods. Download a two-factor authenticator app like Microsoft Authenticator for Android and iOS or Google Authenticator for Android and iOS. Scan the QR Code or enter the key into your authenticator app. The code and time remaining should match the Authenticator App Results.
This series will focus on the utilities I use for this site which implement user security and communications crucial to user registration and user password storage.
I implement two-factor authentication on this site with a Time-Based One-Time Password (TOTP) compatible with authenticator apps like Microsoft Authenticator and Google Authenticator for Android and iOS. I implement the otplib and qrcode libraries for Node.js. The new otplib library v13 and above is a total rewrite from v12. The `totp` and `hotp` specific logic have been moved to their individual packages. This utility demonstrates implementation and functionality.
Open PowerShell or a command prompt from the project folder. Execute the npm CLI (Node Package Manager Command Line Interface) install command to install the 'optlib' and 'qrcode' libraries.
npm install otplib qrcode
The UtilitiesController imports the required otplib functions and the qrcode module. The getRemainingTime function is used for the utility only.
utilities-controller.mjs
import { generateSecret, generate, verify, generateURI } from 'otplib';
import { getRemainingTime } from "@otplib/totp";
import qrcode from 'qrcode';
The UtilitiesController passes template variables to the TOTP Generator template.
utilities-controller.mjs
// Generate a base32 key with length = 32
const authenticatorKey = generateSecret();
// Generate the current token for the key
const token = await generate({ secret: authenticatorKey });
// Format the token string for readability
const formattedToken = token.substring(0, 3) + " " + token.substring(3);
// Displays time remaining and triggers a token refresh
const secondsRemaining = getRemainingTime();
// You can verify the token
const isValid = await verify({ token: token, secret: authenticatorKey });
// Generate an otpauth URI for the Authenticator QR code
const otplibUri = generateURI({ label: 'TOTP Demo User', issuer: 'ExpressWithUsers.Com', secret: authenticatorKey });
// Convert to DataUrl for image source
const qrCodeUrl = await qrcode.toDataURL(otplibUri);
JavaScript in the Express Embedded JavaScript (EJS) template sets the countdown timer and refreshes the token when time remaining reaches zero.
totp-generator.ejs
<script>
let timerInterval = null;
let authenticatorKey = '<%= authenticatorKey %>';
let remainingTimer = '<%= secondsRemaining %>';
const timerLabelSpan = document.querySelector(".timer-label")
const validtokenInput = document.getElementById("validtoken");
const setCircleDasharray = () => {
// Get time remaining path length. Total path length = 283 for 30 seconds
const circleDasharray = `${((remainingTimer - 1) / 30 * 283).toFixed(0)} 283`;
document.getElementById("timer-path-remaining").setAttribute("stroke-dasharray", circleDasharray);
}
const startTimer = () => {
timerInterval = setInterval(() => {
--remainingTimer
let timerLabel = remainingTimer;
if (remainingTimer < 10) timerLabel = `0${remainingTimer}`;
timerLabelSpan.innerHTML = timerLabel;
setCircleDasharray();
if (remainingTimer === 0) fetchCurrentToken();
}, 1000);
}
const fetchCurrentToken = () => {
clearInterval(timerInterval);
let postData = { authenticatorKey };
return fetch('/utilities/api/get-totp-token',
{
method: 'post',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(postData)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Response Not OK');
}
})
.then(responseJSON => {
if (responseJSON.success) {
validtokenInput.value = responseJSON.formattedToken;
remainingTimer = responseJSON.secondsRemaining;
startTimer();
}
else {
showMessageModal(responseJSON.error, 'alert-danger')
}
})
.catch(error => {
showMessageModal(error, 'alert-danger');
});
}
document.addEventListener('DOMContentLoaded', () => {
startTimer();
});
</script>