I Built Production-Ready 2FA (TOTP) in Node.js + Angular
Unlock enhanced security for your applications! This guide walks you through implementing Two-Factor Authentication (TOTP) in Node.js and Angular.
How Do Google Authenticator and Other Apps Implement 2FA?
Have you ever wondered about the technology behind apps like Google Authenticator? Two-Factor Authentication (2FA) significantly boosts account security by requiring two forms of verification. In this detailed guide, I'll walk you through adding Time-Based One-Time Passwords (TOTP) to a full-stack application using Node.js and Angular. You'll get hands-on, working code to quickly set up a 2FA system ready for production.
By the end of this guide, you'll achieve:
- A fully functional backend 2FA system with Speakeasy and JWT.
- An Angular frontend capable of generating QR codes.
- Smooth integration into your current authentication process.
- Enhanced security with temporary tokens.
What Are We Creating?
Our project will cover two primary functionalities:
-
Activating 2FA:
- Users activate 2FA in their profile settings.
- The backend creates a secret key.
- The frontend shows a QR code for the user to scan.
- Users confirm their identity with a 6-digit code.
-
Logging In with 2FA:
- Users submit their email and password.
- On validation and 2FA activation, they get a temporary token.
- Users proceed to a 2FA page to input the 6-digit code.
- The backend verifies the code and grants full session tokens.
How Do We Ensure Security?
We focused on three key security measures:
- Temporary 2FA Tokens: These tokens, expiring in 5 minutes, thwart replay attacks.
- Dedicated 2FA Cookies: Isolating pre-login states from authenticated sessions boosts security.
- Token Version Control: This approach invalidates all sessions when 2FA settings change.
Setting Up the User Model
First, let's update our User model with Prisma:
model User {
id String @id @default(cuid())
email String @unique
password String?
tokenVersion Int @default(0)
// 2FA fields
twoFactorEnabled Boolean @default(false)
twoFactorSecret String? // base32-encoded secret
}
Execute the migration:
npx prisma migrate dev --name add_2fa_fields
We adjust the tokenVersion to invalidate all refresh tokens when users toggle 2FA.
Implementing JWT for Pre-Auth State
We craft a special JWT for the interim between password check and TOTP entry:
import jwt, { Secret } from 'jsonwebtoken';
const TWOFA_SECRET: Secret = process.env.JWT_2FA_SECRET as Secret;
const TWOFA_EXPIRES = '5m'; // Enhances security
export type TwoFAPayload = {
sub: string; // user ID
n: string; // nonce for uniqueness
type: 'twofa'
};
export function signTwoFAToken(userId: string, nonce: string) {
return jwt.sign(
{ sub: userId, n: nonce, type: 'twofa' },
TWOFA_SECRET,
{ expiresIn: TWOFA_EXPIRES }
);
}
export function verifyTwoFAToken(token: string): TwoFAPayload {
const payload = jwt.verify(token, TWOFA_SECRET) as TwoFAPayload;
if (payload.type !== 'twofa') {
throw new Error('Invalid token type');
}
return payload;
}
The nonce ensures token uniqueness, preventing replay attacks.
Managing the 2FA Lifecycle
We define five functions to handle the 2FA process:
- Generate TOTP Secret: Creates a secret key for TOTP.
- Check 2FA Status: Verifies if a user has 2FA enabled.
- Activate 2FA: The user confirms their TOTP code to enable 2FA.
- Deactivate 2FA: Users can turn off 2FA in their profile.
- Verify TOTP for Login: Validates the 6-digit code during login.
Simplified 2FA Controller Functions
Here's a look at the twofaController:
import { Request, Response, NextFunction } from 'express';
import speakeasy from 'speakeasy';
import { prisma } from '../prisma';
import { verifyTwoFAToken, signAccessToken, signRefreshToken } from '../utils/jwt';
export async function twofaSetup(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.userId;
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true },
});
const label = `YourApp:${user.email}`;
const secret = speakeasy.generateSecret({ name: label });
return res.json({
secret: secret.base32,
otpauthUrl: secret.otpauth_url,
});
} catch (err) {
next(err);
}
}
Integrating with Angular Frontend
For Angular integration, we use Angular reactive forms and angularx-qrcode. Install the library with:
npm install angularx-qrcode
Set up the QR code in your profile page:
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-profile',
templateUrl: './profile.page.html',
})
export class ProfilePage implements OnInit {
twoFactorEnabled = false;
setupSecret: string | null = null;
otpauthUrl: string | null = null;
constructor(private auth: AuthService) {}
ngOnInit() {
this.refreshStatus();
}
refreshStatus() {
this.auth.twofaStatus().subscribe(({ enabled }) => {
this.twoFactorEnabled = enabled;
});
}
start2FASetup() {
this.auth.twofaSetup().subscribe(({ secret, otpauthUrl }) => {
this.setupSecret = secret;
this.otpauthUrl = otpauthUrl;
});
}
}
Testing Your Setup
Before launching, complete this checklist:
- [ ] Enable 2FA in profile settings.
- [ ] Verify the QR code displays.
- [ ] Use an authenticator app to scan.
- [ ] Successfully input the code to activate 2FA.
- [ ] Confirm both valid and invalid codes in the login flow.
Conclusion
Congratulations! You've now equipped your Node.js and Angular application with a strong, production-level 2FA system. With Speakeasy for TOTP generation, secure temporary JWT tokens, and a smooth user experience, your application stands better protected against security threats.
Key Insights:
- TOTP implementation drastically improves account security.
- Security measures like token expiration and nonce are crucial against replay attacks.
- Maintaining a seamless user experience is vital when adding security features.
This guide empowers you to confidently integrate 2FA into your applications. Stay tuned for more tutorials and insights by subscribing or following my blog!
Related Articles
Top Highlights from Git 2.52: New Features for Developers
Explore the key features and enhancements in Git 2.52, including improved performance, new functionalities, and user experience upgrades for developers.
Nov 22, 2025
Should We Even Have :closed? Exploring CSS State Management
Explore the debate around the CSS pseudo-class :closed. Is it necessary or does :not(:open) suffice? Dive into coding insights and best practices.
Nov 21, 2025
Build a Multi-Tenant RAG with Fine-Grain Authorization
Discover how to build a multi-tenant RAG system with fine-grained authorization, inspired by Stardew Valley. Perfect for developers looking to enhance their applications!
Nov 21, 2025
