Learn 2FA Authentication - Authentication Foundation: Login & Session
Episode 3 of 23

Learn 2FA Authentication - Authentication Foundation: Login & Session

Before adding the 2FA layer, this episode builds the authentication foundation: password registration and login with bcrypt, HttpOnly session cookies and JWT, the user model structure with a TOTP secret column, and the auth endpoint map that will be injected with the MFA challenge in episode 8.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The 2FA layer doesn't stand alone — it attaches to existing authentication. Episode 3 makes sure that foundation is solid: password registration and login, hashing with bcrypt, HttpOnly cookie-based sessions, the JWT alternative, and the user model structure that will store the TOTP secret.

Why is this episode important? Because almost all 2FA bugs come from a broken foundation: sessions that aren't rotated, plaintext passwords, or a 2FA flag that can be bypassed. With a correct foundation, episode 8 only needs to insert one verification step in the middle of the login flow.

Password Registration and Login

Hashing with bcrypt

Passwords must never be stored in plaintext. Use bcrypt or argon2, which adds a salt automatically and is intentionally slow to slow down offline attacks. Install the hashing library and start the project:

Initialize the project and install auth dependencies
npm init -y
npm install express@5.2.1 bcryptjs express-session cookie-parser

bcryptjs is a pure JavaScript bcrypt implementation that needs no native compilation. On registration, store the hash, not the plaintext; on login, compare the input with bcrypt.compare.

The Registration Flow

When a user signs up, the server validates the input, checks the email is unique, then stores the email and the password hash in the users table. Never return the hash to the client. The login flow simply reverses the process: find the user by email, verify the password, then create a session.

Session and JWT

For browser-based applications, the most secure session storage is in an HttpOnly and Secure cookie — JavaScript can't read the cookie, so it resists XSS. After a successful login, the server signs the session and sends the cookie via the Set-Cookie header.

Example express-session configuration with a secure cookie:

JSSession cookie configuration
app.use(session({
  name: 'sid',
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 30 * 60 * 1000
  }
}));

Notice httpOnly: true and sameSite: 'lax' — both are the first line of defense against XSS and CSRF that we will strengthen in episode 13.

JWT for APIs

If you're building an API without a browser (a mobile app or a pure SPA), JWT is a popular alternative. The server signs a token with claims like sub and exp, the client stores it and sends it in the Authorization header. The weakness: a token can't be revoked server-side before it expires, so server-side sessions are generally preferred for a 2FA feature. Episode 15 will discuss the relationship between JWT and the 2FA flag in detail.

The 2FA Feature Structure in the User Model

Added Columns

The 2FA foundation requires several new columns on the user model. The TOTP secret is stored encrypted (episode 7), not in plaintext:

User schema with 2FA columns
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  totp_secret_encrypted TEXT,
  totp_enabled BOOLEAN NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

The three key columns are totp_secret_encrypted, totp_enabled, and later a recovery codes table. The totp_secret_encrypted column stays empty until enrollment completes in episode 6.

Recovery Codes

Besides the columns above, add a recovery_codes table that stores the hashes of single-use recovery codes. Episode 9 will dissect their generation and lifecycle, but the table structure can be created now so the migration runs in one pass.

Auth Endpoints That Will Get 2FA Injected

The auth endpoint map in your project now looks like this. The 2FA challenge will slot in between POST /login and the creation of the full session in episode 8:

Auth endpoint map
POST /register            -> create user + hash password
POST /login               -> verify password, set pendingMfa if 2FA is active
POST /login/mfa           -> verify TOTP, grant full session
POST /logout              -> destroy session
GET  /account/settings    -> 2FA management page

Note that POST /login doesn't immediately grant a full session when 2FA is active — it only marks pendingMfa. This is the bridge to episode 8.

Conclusion

Episode 3 laid the authentication foundation: passwords hashed with bcrypt, sessions stored in HttpOnly cookies, JWT as the API alternative, a user model with TOTP secret columns, and the endpoint map waiting for the MFA challenge.

The key takeaways:

  • Store passwords as bcrypt or argon2 hashes, never plaintext.
  • Session cookies must be HttpOnly, Secure, and SameSite.
  • JWT suits APIs but is hard to revoke before expiry.
  • The user model adds totp_secret_encrypted and totp_enabled.
  • Recovery codes are stored separately as single-use hashes.
  • POST /login only grants a full session after MFA completes.

In the next episode, episode 4, we will cover generating the secret and the otpauth URI — creating a random secret per user with authenticator.generateSecret and assembling the provisioning URI that Google Authenticator recognizes.

Learn 2FA Authentication - Authentication Foundation: Login & Session | Learn 2FA Authentication