Multi-Factor Authentication (MFA) is a method of verifying a user’s identity by requiring more than one form of evidence, such as something they know, something they have, and something they are. In this guide, we’ll dive into implementing two popular MFA methods: Time-Based One-Time Passwords (TOTP) and Web Authentication (WebAuthn).
What is Time-Based One-Time Password (TOTP)?
Time-Based One-Time Password (TOTP) is a type of one-time password algorithm that generates a unique passcode every 30 seconds based on a shared secret key between the authentication server and the user’s device. TOTP is widely used in applications like Google Authenticator, Authy, and many others.
What is Web Authentication (WebAuthn)?
Web Authentication (WebAuthn) is a W3C standard that enables strong, phishing-resistant authentication using public key cryptography. Unlike TOTP, which relies on a shared secret, WebAuthn uses asymmetric keys generated by the authenticator (such as a hardware security key or built-in authenticator in devices like smartphones and laptops).
Why choose TOTP and WebAuthn for MFA?
TOTP and WebAuthn offer different strengths:
- TOTP: Easy to implement, widely supported, and doesn’t require special hardware.
- WebAuthn: More secure, resistant to phishing, and supports biometric authentication methods.
Setting up TOTP
Let’s start by setting up TOTP for MFA.
Step-by-step Guide
Install a TOTP library
Choose a library that suits your programming language. For Node.js, `speakeasy` is a good choice.Generate a secret key
Create a secret key that will be shared between the server and the user's device.Display the QR code
Encode the secret key into a QR code that the user can scan with their TOTP app.Verify the TOTP code
Check the TOTP code provided by the user against the expected value generated by the server.Code Example
Here’s how you can set up TOTP using the speakeasy library in Node.js.
Install the library
npm install speakeasy qrcode
Generate a secret key and QR code
const speakeasy = require('speakeasy');
const qr = require('qrcode');
// Generate a secret key
const secret = speakeasy.generateSecret({ length: 20 });
// Display the QR code URL
qr.toDataURL(secret.otpauth_url, function(err, image_data) {
console.log(image_data); // This is the QR code URL
});
Verify the TOTP code
// Assume `token` is the code entered by the user
const token = '123456';
// Verify the token
const verified = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: token
});
console.log(verified ? 'Token is valid' : 'Invalid token');
Security Considerations
Setting up WebAuthn
Next, let’s integrate WebAuthn into your application.
Step-by-step Guide
Register the user
Create a registration ceremony where the user's authenticator generates a public/private key pair.Store the public key
Save the public key generated during registration for later verification.Authenticate the user
Initiate an authentication ceremony where the user proves possession of the private key.Verify the signature
Check the signature provided by the user's authenticator to ensure it's valid.Code Example
Here’s a basic example using the simple-webauthn-server and simple-webauthn-browser libraries.
Install the libraries
npm install @simplewebauthn/server @simplewebauthn/browser
Register the user
Server-side
const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');
// Generate registration options
const registrationOptions = generateRegistrationOptions({
rpName: 'Example Corp.',
rpID: 'example.com',
userID: 'unique-user-id',
userName: '[email protected]',
userDisplayName: 'John Doe',
attestationType: 'none',
supportedAlgorithmIDs: [-7, -257],
});
// Send `registrationOptions` to the client
Client-side
import { startRegistration } from '@simplewebauthn/browser';
// Assume `registrationOptions` is received from the server
const credential = await startRegistration(registrationOptions);
// Send `credential` back to the server
Server-side (verify)
const expectedChallenge = 'expected-challenge'; // Store this securely during registration
const verification = await verifyRegistrationResponse({
credential: credential,
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
});
const { verified, registrationInfo } = verification;
if (verified) {
const { credentialPublicKey, credentialID, counter } = registrationInfo;
// Save `credentialPublicKey`, `credentialID`, and `counter` for future authentication
}
Authenticate the user
Server-side
const { generateAuthenticationOptions, verifyAuthenticationResponse } = require('@simplewebauthn/server');
// Generate authentication options
const authenticationOptions = generateAuthenticationOptions({
allowCredentials: [
{
id: Buffer.from('credentialID', 'base64url'),
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal'],
},
],
userVerification: 'preferred',
});
// Send `authenticationOptions` to the client
Client-side
import { startAuthentication } from '@simplewebauthn/browser';
// Assume `authenticationOptions` is received from the server
const assertion = await startAuthentication(authenticationOptions);
// Send `assertion` back to the server
Server-side (verify)
const expectedChallenge = 'expected-challenge'; // Store this securely during authentication
const verification = await verifyAuthenticationResponse({
credential: assertion,
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
authenticator: {
credentialPublicKey: Buffer.from('credentialPublicKey', 'base64url'),
credentialID: Buffer.from('credentialID', 'base64url'),
counter: 1, // The counter value from the previous authentication
},
});
const { verified, authenticationInfo } = verification;
if (verified) {
const { newCounter } = authenticationInfo;
// Update the counter value in your database
}
Security Considerations
Comparison: TOTP vs WebAuthn
| Approach | Pros | Cons | Use When |
|---|---|---|---|
| TOTP | Easy to implement, widely supported | Less secure, vulnerable to phishing | Basic MFA requirement, no special hardware |
| WebAuthn | More secure, phishing-resistant, supports biometrics | Requires user consent, some devices may not support it | Strong security, high assurance required |
Quick Reference
📋 Quick Reference
speakeasy.generateSecret()- Generates a secret key for TOTPspeakeasy.totp.verify()- Verifies a TOTP codegenerateRegistrationOptions()- Generates options for WebAuthn registrationverifyRegistrationResponse()- Verifies WebAuthn registration responsegenerateAuthenticationOptions()- Generates options for WebAuthn authenticationverifyAuthenticationResponse()- Verifies WebAuthn authentication response
Troubleshooting Common Issues
Error: Invalid TOTP code
Ensure that:
- The secret key is correctly shared between the server and the user’s device.
- The server’s clock is synchronized with NTP.
Error: Registration failed
Check that:
- The user’s authenticator supports the required algorithms.
- The challenge is unique and unpredictable.
Error: Authentication failed
Verify that:
- The public key and counter are correctly stored and retrieved.
- The challenge matches the expected value.
Key Takeaways
🎯 Key Takeaways
- TOTP is easy to implement but less secure compared to WebAuthn.
- WebAuthn offers stronger security and supports biometric authentication.
- Both methods require careful handling of secrets and challenges to prevent security vulnerabilities.
Implementing TOTP and WebAuthn in your application can significantly enhance security by adding an additional layer of authentication. Choose the method that best fits your security requirements and user base. Happy coding!

