Authentication Models and Workflows

Module 26: Advanced Backend & API Development

Introduction to Authentication

Authentication is the process of verifying that users are who they claim to be. It's the digital equivalent of checking someone's ID at the door before letting them into a restricted area. In today's interconnected world, robust authentication is the foundation of security for web applications and APIs.

graph TD A[User] -->|Presents Credentials| B[Authentication System] B -->|Verification Success| C[Access Granted] B -->|Verification Failure| D[Access Denied] C -->|Session/Token Created| E[Protected Resources] E -->|Access Control| F[Authorization]

When we talk about authentication, we're addressing three primary questions:

Think of authentication like entering a country: your passport establishes your identity, the immigration officer verifies it's really you, and then you receive a visa or entry stamp that allows you to remain in the country for a specified period without having to verify your identity again at every internal checkpoint.

Authentication Factors

Authentication factors are categories of credentials used to verify identity. Modern systems often combine multiple factors for enhanced security.

Something You Know

Knowledge-based authentication relies on information only the user should know.

Something You Have

Possession-based authentication requires the user to have physical access to an authentication device.

Something You Are

Biometric authentication verifies physical or behavioral characteristics.

Somewhere You Are

Location-based authentication validates the user's geographic position.

graph TD A[Authentication] --> B[Single-Factor] A --> C[Multi-Factor] B --> D[Something You Know] C --> D C --> E[Something You Have] C --> F[Something You Are] C --> G[Somewhere You Are]

The bank ATM is a classic real-world example of multi-factor authentication: you need both your bank card (something you have) and your PIN (something you know) to access your account.

Password-Based Authentication

Despite their limitations, passwords remain the most common authentication mechanism. Let's examine how modern password-based authentication should be implemented.

Password Storage

Never store passwords in plain text! Always use a secure one-way hashing algorithm with a unique salt for each user.

flowchart LR A[Password] --> B[Add Salt] B --> C[Hash Function] C --> D[Hashed Password] E[Stored in Database] --> F[Salt] E --> D

// Node.js example using bcrypt
const bcrypt = require('bcrypt');
const saltRounds = 12; // Higher is more secure but slower

async function storePassword(password) {
  try {
    // bcrypt automatically generates a salt and combines it with the hash
    const hashedPassword = await bcrypt.hash(password, saltRounds);
    // Store hashedPassword in your database
    return hashedPassword;
  } catch (error) {
    console.error('Error hashing password:', error);
    throw error;
  }
}

async function verifyPassword(password, storedHash) {
  try {
    // bcrypt will extract the salt from the stored hash
    // and use it to hash the provided password for comparison
    const match = await bcrypt.compare(password, storedHash);
    return match; // true if passwords match, false otherwise
  } catch (error) {
    console.error('Error verifying password:', error);
    throw error;
  }
}
            

Think of password hashing like a one-way meat grinder: you can turn a steak into ground beef, but you can't reconstruct the original steak from the ground meat. Similarly, a properly hashed password can't be reversed to reveal the original password.

Password Strength Requirements


// Example password strength validation using zxcvbn
const zxcvbn = require('zxcvbn');

function validatePasswordStrength(password, userInputs = []) {
  // userInputs should include data like username, email, etc.
  const result = zxcvbn(password, userInputs);
  
  // Score ranges from 0 (very weak) to 4 (very strong)
  if (result.score < 3) {
    return {
      valid: false,
      message: result.feedback.warning || 'Password is too weak',
      suggestions: result.feedback.suggestions
    };
  }
  
  return { valid: true };
}
            

Session-Based Authentication

Session-based authentication is a traditional model where the server maintains the user's authentication state.

sequenceDiagram participant User participant Server participant Database User->>Server: Login with credentials Server->>Database: Verify credentials Database-->>Server: Credentials valid Server->>Server: Generate session ID Server->>Database: Store session ID & user data Server-->>User: Set session cookie Note over User,Server: Later requests User->>Server: Request with session cookie Server->>Database: Validate session Database-->>Server: Session valid Server-->>User: Requested resource

Session Workflow

  1. User provides credentials
  2. Server validates credentials
  3. Server creates a unique session identifier
  4. Server stores session data (in memory, database, or distributed cache)
  5. Server sends session ID to client (usually as a cookie)
  6. Client includes session ID with subsequent requests
  7. Server validates session ID and retrieves session data
  8. When user logs out, server invalidates the session

Express.js Session Implementation


const express = require('express');
const session = require('express-session');
const redis = require('redis');
const RedisStore = require('connect-redis')(session);

const app = express();

// Create Redis client
const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379
});

// Configure session middleware
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'your-secure-session-secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only in production
    httpOnly: true, // Prevent JavaScript access
    maxAge: 1000 * 60 * 60 * 24 // 24 hours
  }
}));

// Login route
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // Validate credentials (implementation details omitted)
    const user = await validateUser(username, password);
    
    if (user) {
      // Store user info in session
      req.session.userId = user.id;
      req.session.role = user.role;
      
      // Regenerate session ID to prevent session fixation
      req.session.regenerate((err) => {
        if (err) {
          console.error('Session regeneration error:', err);
          return res.status(500).json({ error: 'Authentication error' });
        }
        
        res.json({ message: 'Login successful' });
      });
    } else {
      res.status(401).json({ error: 'Invalid credentials' });
    }
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Protected route
app.get('/profile', (req, res) => {
  // Check if user is authenticated
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  // Fetch and return user profile
  // ...
});

// Logout route
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      console.error('Session destruction error:', err);
      return res.status(500).json({ error: 'Logout error' });
    }
    
    res.clearCookie('connect.sid');
    res.json({ message: 'Logout successful' });
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
            

Advantages of Session-Based Authentication

Disadvantages of Session-Based Authentication

Think of session-based authentication like a concert wristband: once you show your ticket (credentials) at the entrance, you get a wristband (session cookie) that lets you enter and exit the venue without showing your ticket again during the event.

Token-Based Authentication

Token-based authentication is a stateless model where the client stores the authentication state in a secure token.

sequenceDiagram participant User participant Server User->>Server: Login with credentials Server->>Server: Verify credentials Server->>Server: Generate signed token Server-->>User: Return token Note over User,Server: Later requests User->>Server: Request with token in header Server->>Server: Verify token signature Server->>Server: Decode token payload Server-->>User: Requested resource

Token Workflow

  1. User provides credentials
  2. Server validates credentials
  3. Server generates a signed token containing user identity and permissions
  4. Server sends the token to the client
  5. Client stores the token (localStorage, sessionStorage, or memory)
  6. Client includes token with subsequent requests (usually in Authorization header)
  7. Server verifies token signature and extracts user information
  8. When user logs out, client discards the token

Popular Token Formats

JWT Structure

graph LR A[Header] -->|Base64Url Encoded| D[xxxxx] B[Payload] -->|Base64Url Encoded| E[yyyyy] C[Signature] -->|HMAC/RSA| F[zzzzz] D --> G[Token] E --> G F --> G G --> H["xxxxx.yyyyy.zzzzz"]

Implementing JWT Authentication


// Node.js example using Express and jsonwebtoken
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const app = express();
app.use(express.json());

const JWT_SECRET = process.env.JWT_SECRET || 'your-secure-jwt-secret';
const JWT_EXPIRES_IN = '1h';

// Login route
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // Find user (implementation details omitted)
    const user = await findUserByUsername(username);
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Verify password
    const passwordValid = await bcrypt.compare(password, user.password);
    
    if (!passwordValid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Generate JWT
    const token = jwt.sign(
      {
        userId: user.id,
        username: user.username,
        role: user.role
      },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    
    // Return token to client
    res.json({
      message: 'Login successful',
      token,
      expiresIn: JWT_EXPIRES_IN
    });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Authentication middleware
function authenticateToken(req, res, next) {
  // Get token from Authorization header
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
  
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  
  // Verify token
  jwt.verify(token, JWT_SECRET, (err, decoded) => {
    if (err) {
      console.error('Token verification error:', err);
      return res.status(403).json({ error: 'Invalid or expired token' });
    }
    
    // Add user info to request object
    req.user = decoded;
    next();
  });
}

// Protected route
app.get('/api/profile', authenticateToken, (req, res) => {
  // req.user contains the decoded JWT payload
  res.json({
    userId: req.user.userId,
    username: req.user.username,
    role: req.user.role
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
            

Advantages of Token-Based Authentication

Disadvantages of Token-Based Authentication

Think of token-based authentication like a temporary ID badge with your photo, name, and access levels printed on it. Once issued, the badge can be visually inspected at each checkpoint without needing to contact the central security office, but it can't be remotely deactivated if lost.

Refresh Token Pattern

The refresh token pattern improves security by using short-lived access tokens alongside longer-lived refresh tokens.

sequenceDiagram participant User participant Server participant Database User->>Server: Login with credentials Server->>Server: Verify credentials Server->>Server: Generate access token (short-lived) Server->>Server: Generate refresh token (long-lived) Server->>Database: Store refresh token & metadata Server-->>User: Return both tokens Note over User,Server: Access token expires User->>Server: Request with expired access token Server-->>User: 401 Unauthorized User->>Server: Request new access token with refresh token Server->>Database: Validate refresh token Database-->>Server: Refresh token valid Server->>Server: Generate new access token Server-->>User: Return new access token

Implementation


// Token generation during login
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // Verify credentials (implementation omitted)
    const user = await validateUser(username, password);
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Generate access token (short-lived)
    const accessToken = jwt.sign(
      {
        userId: user.id,
        username: user.username,
        role: user.role
      },
      ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' } // Short lifespan
    );
    
    // Generate refresh token (long-lived)
    const refreshToken = crypto.randomBytes(40).toString('hex');
    const refreshTokenExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
    
    // Store refresh token in database
    await storeRefreshToken({
      token: refreshToken,
      userId: user.id,
      expiresAt: refreshTokenExpiry,
      userAgent: req.headers['user-agent'],
      ipAddress: req.ip
    });
    
    // Return both tokens
    res.json({
      accessToken,
      refreshToken,
      expiresIn: '15m'
    });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Token refresh endpoint
app.post('/api/refresh-token', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(400).json({ error: 'Refresh token required' });
  }
  
  try {
    // Find refresh token in database
    const storedToken = await findRefreshToken(refreshToken);
    
    if (!storedToken || new Date() > storedToken.expiresAt) {
      return res.status(401).json({ error: 'Invalid or expired refresh token' });
    }
    
    // Get user associated with token
    const user = await findUserById(storedToken.userId);
    
    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }
    
    // Generate new access token
    const accessToken = jwt.sign(
      {
        userId: user.id,
        username: user.username,
        role: user.role
      },
      ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' }
    );
    
    // Return new access token
    res.json({
      accessToken,
      expiresIn: '15m'
    });
  } catch (error) {
    console.error('Token refresh error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Logout (invalidate refresh token)
app.post('/api/logout', authenticateToken, async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(400).json({ error: 'Refresh token required' });
  }
  
  try {
    // Remove refresh token from database
    await removeRefreshToken(refreshToken);
    res.json({ message: 'Logout successful' });
  } catch (error) {
    console.error('Logout error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});
            

Security Considerations

The refresh token pattern is like having a hotel key card (access token) that expires daily, but as long as you have your valid reservation confirmation (refresh token), you can get a new key card from the front desk without having to check in again.

Multi-Factor Authentication (MFA)

Multi-factor authentication significantly increases security by requiring users to verify their identity through multiple independent channels.

flowchart LR A[Login with Password] --> B{Password Valid?} B -->|No| C[Access Denied] B -->|Yes| D[Request Second Factor] D --> E[Send Code via SMS/Email] D --> F[Generate TOTP Code] D --> G[Push Notification] E --> H{Second Factor Valid?} F --> H G --> H H -->|No| C H -->|Yes| I[Access Granted]

Common Second Factors

Implementing TOTP-Based MFA


// Node.js example using speakeasy for TOTP
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

// Generate secret during MFA setup
app.post('/api/setup-mfa', authenticateToken, async (req, res) => {
  try {
    // Generate a secret key
    const secret = speakeasy.generateSecret({
      name: `MyApp:${req.user.username}`
    });
    
    // Store secret key in user's record (temporarily) 
    // In production, encrypt this before storage
    await updateUserMfaSecret(req.user.userId, secret.base32);
    
    // Generate QR code for easy setup with authenticator apps
    const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
    
    res.json({
      message: 'MFA setup initiated',
      secret: secret.base32, // Only show during setup
      qrCodeUrl
    });
  } catch (error) {
    console.error('MFA setup error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Verify and enable MFA
app.post('/api/verify-mfa', authenticateToken, async (req, res) => {
  const { token } = req.body;
  
  try {
    // Get user's secret
    const user = await findUserById(req.user.userId);
    
    if (!user.mfaSecret) {
      return res.status(400).json({ error: 'MFA not set up' });
    }
    
    // Verify the token
    const verified = speakeasy.totp.verify({
      secret: user.mfaSecret,
      encoding: 'base32',
      token: token,
      window: 1 // Allow 1 step before/after for clock drift
    });
    
    if (!verified) {
      return res.status(401).json({ error: 'Invalid MFA token' });
    }
    
    // Enable MFA for the user
    await updateUserMfaEnabled(req.user.userId, true);
    
    res.json({ message: 'MFA enabled successfully' });
  } catch (error) {
    console.error('MFA verification error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Login with MFA
app.post('/api/login', async (req, res) => {
  const { username, password, mfaToken } = req.body;
  
  try {
    // Validate credentials
    const user = await validateUser(username, password);
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Check if MFA is enabled
    if (user.mfaEnabled) {
      // If MFA token not provided in initial request
      if (!mfaToken) {
        return res.status(200).json({
          message: 'MFA required',
          requireMfa: true,
          userId: user.id // Include minimal info needed for MFA step
        });
      }
      
      // Verify MFA token
      const verified = speakeasy.totp.verify({
        secret: user.mfaSecret,
        encoding: 'base32',
        token: mfaToken,
        window: 1
      });
      
      if (!verified) {
        return res.status(401).json({ error: 'Invalid MFA token' });
      }
    }
    
    // At this point, user is authenticated (with MFA if required)
    // Generate tokens as usual...
    
    res.json({
      message: 'Login successful',
      accessToken,
      refreshToken
    });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});
            

Mobile and API Authentication

Mobile applications and APIs require special consideration for authentication flows.

Mobile-Specific Considerations

API Authentication Best Practices

Native App Authentication Flow

sequenceDiagram participant A as Mobile App participant B as Backend API A->>B: Login Request B->>B: Authenticate User B-->>A: Access Token & Refresh Token A->>A: Store Tokens Securely Note over A,B: Later Requests A->>B: API Request with Access Token B-->>A: Response Note over A,B: When Access Token Expires A->>B: API Request with Expired Token B-->>A: 401 Unauthorized A->>B: Refresh Request with Refresh Token B-->>A: New Access Token A->>B: Retry Original Request B-->>A: Response

// Swift example for secure token storage on iOS
import Foundation
import Security

class KeychainService {
    static func saveToken(_ token: String, forKey key: String) -> Bool {
        let tokenData = token.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: tokenData,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        
        // Delete any existing token
        SecItemDelete(query as CFDictionary)
        
        // Add the new token
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }
    
    static func getToken(forKey key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        guard status == errSecSuccess,
            let tokenData = result as? Data,
            let token = String(data: tokenData, encoding: .utf8) else {
            return nil
        }
        
        return token
    }
    
    static func deleteToken(forKey key: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

// Usage
class AuthService {
    private let accessTokenKey = "com.myapp.accessToken"
    private let refreshTokenKey = "com.myapp.refreshToken"
    
    func saveTokens(accessToken: String, refreshToken: String) {
        KeychainService.saveToken(accessToken, forKey: accessTokenKey)
        KeychainService.saveToken(refreshToken, forKey: refreshTokenKey)
    }
    
    func getAccessToken() -> String? {
        return KeychainService.getToken(forKey: accessTokenKey)
    }
    
    func getRefreshToken() -> String? {
        return KeychainService.getToken(forKey: refreshTokenKey)
    }
    
    func clearTokens() {
        KeychainService.deleteToken(forKey: accessTokenKey)
        KeychainService.deleteToken(forKey: refreshTokenKey)
    }
}
            

Social Authentication

Social authentication allows users to log in using their existing accounts from social platforms.

sequenceDiagram participant U as User participant A as Your App participant S as Social Provider U->>A: Click "Login with Social" A->>S: Redirect to Social Login U->>S: Enter Social Credentials S->>S: Authenticate User S->>A: Redirect with Auth Code A->>S: Exchange Code for Token S-->>A: Access Token & User Info A->>A: Create or Update User A-->>U: Login Complete

Benefits

Challenges

Implementation with Passport.js


const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');

const app = express();

// Configure session
app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: false
}));

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

// Configure Google Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/auth/google/callback'
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Find or create user based on Google profile
      let user = await findUserByProviderId('google', profile.id);
      
      if (!user) {
        // Create new user if not found
        user = await createUser({
          provider: 'google',
          providerId: profile.id,
          email: profile.emails[0].value,
          name: profile.displayName,
          picture: profile.photos[0].value
        });
      }
      
      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// Serialize/deserialize user for session
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await findUserById(id);
    done(null, user);
  } catch (error) {
    done(error);
  }
});

// Routes for Google authentication
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    // Successful authentication
    res.redirect('/dashboard');
  }
);

// Check if user is authenticated
function isAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
}

// Protected route
app.get('/dashboard', isAuthenticated, (req, res) => {
  res.json({ user: req.user });
});

// Logout
app.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
            

Security Considerations

Authentication security requires attention to several key areas:

Common Attack Vectors

Best Practices

Rate Limiting Implementation


// Express rate limiting example using express-rate-limit
const rateLimit = require('express-rate-limit');

// Create limiter for login attempts
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: {
    error: 'Too many login attempts. Please try again after 15 minutes.'
  },
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false // Disable the `X-RateLimit-*` headers
});

// Apply rate limiting to login route
app.post('/api/login', loginLimiter, async (req, res) => {
  // Login logic...
});

// Create a more lenient limiter for general API requests
const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
  message: {
    error: 'Too many requests. Please try again later.'
  }
});

// Apply to all API routes
app.use('/api/', apiLimiter);
            

Authentication Workflows for Different Application Types

Single-Page Applications (SPAs)

Server-Rendered Web Applications

Mobile Applications

Microservices

IoT Devices

Practical Activities

Activity 1: Implement Password Authentication

Create a basic Express application with secure password-based authentication:

  1. Set up an Express server with MongoDB or another database
  2. Create user registration and login endpoints
  3. Implement proper password hashing with bcrypt
  4. Add session-based authentication
  5. Create protected routes that require authentication
  6. Implement logout functionality

Activity 2: Token-Based Authentication

Modify your application to use JWT authentication:

  1. Replace session-based auth with JWT tokens
  2. Create an authentication middleware to validate tokens
  3. Implement the refresh token pattern
  4. Add token blacklisting for logout
  5. Test with a simple frontend or API client

Activity 3: Multi-Factor Authentication

Add MFA to your authentication system:

  1. Integrate a TOTP library like speakeasy
  2. Create endpoints for MFA setup and verification
  3. Generate QR codes for easy setup
  4. Modify the login flow to require MFA when enabled
  5. Add recovery codes for backup access

Activity 4: Social Authentication

Add social login to your application:

  1. Register your app with Google, Facebook, or another provider
  2. Integrate Passport.js with the appropriate strategies
  3. Implement the OAuth flow
  4. Handle user creation/linking when social users login
  5. Add profile information synchronization

Additional Resources

Books and Documentation

Learning Resources

Libraries and Tools