Phase 4: User Authentication Routes (With Detailed Comments)

← Back to Main Tutorial

Understanding the Problem

In this phase, we need to create the authentication API routes for our Express application. These routes will allow users to:

These routes form the foundation of user authentication in any web application. They allow users to establish and manage their identity within your system, which is crucial for personalized experiences and data security.

RESTful API Design: We're following REST principles for our API endpoints:

  • Resource-oriented: Routes are organized around resources (users and sessions)
  • HTTP methods match actions: POST for creation, GET for retrieval, DELETE for removal
  • Consistent URL patterns: Clear, predictable URL structures

API Endpoints as User Interfaces: Although invisible to end-users, API endpoints serve as the interface between your frontend and backend. Like any good interface, they should be:

  • Intuitive and predictable
  • Well-documented
  • Consistent in behavior and responses
  • Robust against errors

Planning the Solution

We will create the following API routes:

For each route, we will follow a feature-branch Git workflow to keep our development process organized and allow for more focused testing before merging changes into our main development branch.

Setting Up Route Files

First, let's create the necessary router files to organize our API endpoints. We'll create separate files for session-related routes and user-related routes.

Creating the Session Router

Create a file called session.js in the backend/routes/api folder to handle session-related routes:

// backend/routes/api/session.js
const express = require('express');          // Import Express
const router = express.Router();             // Create a router instance

module.exports = router;                     // Export the router for use in other files

Router Organization: Express allows us to create modular route handlers using express.Router(). This helps organize our code by:

  • Grouping related routes together (e.g., all session-related endpoints)
  • Keeping files smaller and more focused
  • Making routes more maintainable and easier to test

Each router can have its own middleware and route handlers, and then be "mounted" at a specific URL prefix in the main application.

Creating the Users Router

Create a file called users.js in the backend/routes/api folder to handle user-related routes:

// backend/routes/api/users.js
const express = require('express');          // Import Express
const router = express.Router();             // Create a router instance

module.exports = router;                     // Export the router

Connecting the Routers

Now, let's update the backend/routes/api/index.js file to connect these routers to our API:

// backend/routes/api/index.js
const router = require('express').Router();            // Create main API router
const sessionRouter = require('./session.js');         // Import session router
const usersRouter = require('./users.js');             // Import users router
const { restoreUser } = require("../../utils/auth.js"); // Import auth middleware

// Connect restoreUser middleware to the API router
// If current user session is valid, set req.user to the user in the database
// If current user session is not valid, set req.user to null
router.use(restoreUser);                               // Apply restoreUser to all API routes

router.use('/session', sessionRouter);                 // Mount session router at /api/session
router.use('/users', usersRouter);                     // Mount users router at /api/users

router.post('/test', (req, res) => {                   // Test route (can be removed later)
  res.json({ requestBody: req.body });
});

module.exports = router;                               // Export the configured router

Note that we're connecting the restoreUser middleware before connecting our routers. This ensures that the req.user property is set for all routes, which is essential for our authentication logic.

Middleware Order: The order of middleware is crucial in Express. Here's why we put restoreUser before mounting the routers:

// This sequence:
router.use(restoreUser);
router.use('/session', sessionRouter);
router.use('/users', usersRouter);

// Ensures that for any request to /api/session/* or /api/users/*:
// 1. First, restoreUser middleware runs and sets req.user
// 2. Then, the request is passed to the appropriate router
                

Router Mounting: When we use router.use('/path', someRouter), we're "mounting" that router at the specified path. This means:

  • All routes defined in sessionRouter will be prefixed with /api/session
  • All routes defined in usersRouter will be prefixed with /api/users

For example, if sessionRouter defines a route for POST /, it will be accessible at POST /api/session.

Implementing Login Functionality

Creating a Login Feature Branch

Let's start by creating a feature branch for the login functionality:

git checkout dev                    # Switch to dev branch
git checkout -b login               # Create and switch to login branch

Implementing the Login Route

Update the backend/routes/api/session.js file to include the login route:

// backend/routes/api/session.js
const express = require('express');
const { Op } = require('sequelize');           // Import Sequelize operators
const bcrypt = require('bcryptjs');            // Import bcrypt for password comparison

const { setTokenCookie, restoreUser } = require('../../utils/auth');  // Import auth utilities
const { User } = require('../../db/models');   // Import User model

const router = express.Router();

// Log in
router.post('/', async (req, res, next) => {   // POST /api/session endpoint
  const { credential, password } = req.body;   // Extract credentials from request body

  // Find the user by either username or email
  const user = await User.unscoped().findOne({  // Use unscoped() to include hashedPassword
    where: {
      [Op.or]: {                              // Use OR operator to match either field
        username: credential,
        email: credential
      }
    }
  });

  // Check if user exists and password is correct
  if (!user || !bcrypt.compareSync(password, user.hashedPassword.toString())) {
    const err = new Error('Login failed');     // Create error for failed login
    err.status = 401;                          // Set HTTP status code to 401 Unauthorized
    err.title = 'Login failed';
    err.errors = { credential: 'The provided credentials were invalid.' };
    return next(err);                          // Pass error to error-handling middleware
  }

  // Create a safe user object (without hashedPassword)
  const safeUser = {
    id: user.id,
    email: user.email,
    username: user.username,
  };

  // Set the JWT cookie
  await setTokenCookie(res, safeUser);         // Create and set JWT cookie

  // Return the user information
  return res.json({
    user: safeUser                            // Return user data as JSON
  });
});

module.exports = router;

Let's break down what this login route does:

  1. It extracts the credential (which can be either a username or email) and password from the request body.
  2. It finds a user with a matching username or email using Sequelize's Op.or operator.
  3. It calls User.unscoped() to temporarily remove the default scope, which allows us to access the hashedPassword field that's normally excluded.
  4. It checks if a user was found and if the provided password matches the stored hashed password.
  5. If either check fails, it creates an error and passes it to the next error-handling middleware.
  6. If the credentials are valid, it creates a safe user object (without sensitive information like the hashed password).
  7. It calls setTokenCookie to set a JWT cookie containing the user's information.
  8. Finally, it returns a JSON response with the user's information.

User-Friendly Login: This implementation allows users to log in with either their username or email, which is a common and user-friendly pattern. The Op.or operator creates a SQL query like:

WHERE username = 'credential' OR email = 'credential'
                

Model Scoping and Security: We use User.unscoped() to temporarily remove the default scope we set in Phase 3. This is necessary because:

  • Our default scope excludes hashedPassword for security
  • But for login, we need to access hashedPassword to verify credentials
  • unscoped() gives us access to all fields for this specific query only

Password Verification: bcrypt.compareSync(plainPassword, hashedPassword) handles the secure comparison of passwords:

  • It takes the plain text password from the request
  • Hashes it with the same salt that was used to create the stored hash
  • Compares the resulting hash with the stored hash
  • Returns true only if they match

Error Handling: If authentication fails, we create a descriptive error object and pass it to next(err), which sends it to our error-handling middleware from Phase 2. This provides a consistent error response format.

Testing the Login Route

Now, let's test the login route to ensure it's working correctly. Start your server if it's not already running:

cd backend
npm start

First, get a CSRF token by navigating to http://localhost:8000/api/csrf/restore. Then, open your browser's developer console and use fetch to test the login route.

Test logging in with a username:

fetch('/api/session', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",               // Specify JSON content
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`      // Include CSRF token
  },
  body: JSON.stringify({ credential: 'Demo-lition', password: 'password' })  // Send credentials
}).then(res => res.json()).then(data => console.log(data));

Test logging in with an email:

fetch('/api/session', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`
  },
  body: JSON.stringify({ credential: 'demo@user.io', password: 'password' })
}).then(res => res.json()).then(data => console.log(data));

Test with invalid credentials:

fetch('/api/session', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`
  },
  body: JSON.stringify({ credential: 'Demo-lition', password: 'wrongpassword' })
}).then(res => res.json()).then(data => console.log(data));

If everything is working correctly, you should see a successful login response for the first two requests (with a token cookie set) and an error response for the third request.

Testing with Fetch API: The Fetch API is a modern way to make HTTP requests from the browser. We're using it here to test our endpoints directly from the browser console.

CSRF Protection: For any non-GET request (like POST), our application requires a CSRF token to be included in the headers. This token must match the one in the XSRF-TOKEN cookie. This protects against Cross-Site Request Forgery attacks.

Expected Response Format: A successful login should return:

{
  "user": {
    "id": 1,
    "email": "demo@user.io",
    "username": "Demo-lition"
  }
}
                

And an invalid login should return an error like:

{
  "title": "Login failed",
  "message": "Login failed",
  "errors": {
    "credential": "The provided credentials were invalid."
  },
  "stack": "..."  // (in development mode only)
}
                

Committing and Merging the Login Branch

Once you've confirmed that the login functionality is working, commit your changes and merge them into the dev branch:

git add .                           # Stage all changes
git commit -m "Add User login backend endpoint"  # Commit with descriptive message
git checkout dev                    # Switch back to dev branch
git pull origin dev                 # If collaborating with others
git merge login                     # Merge login branch into dev
git push origin dev                 # Push changes to remote repository

Implementing Logout Functionality

Creating a Logout Feature Branch

Let's create a new feature branch for the logout functionality:

git checkout dev
git checkout -b logout

Implementing the Logout Route

Update the backend/routes/api/session.js file to include the logout route:

// backend/routes/api/session.js
// ... existing code

// Log out
router.delete('/', (_req, res) => {            // DELETE /api/session endpoint
  res.clearCookie('token');                    // Remove the JWT cookie
  return res.json({ message: 'success' });     // Return success message
});

module.exports = router;

The logout route is much simpler than the login route. It simply clears the JWT token cookie and returns a success message. Notice that we use an underscore prefix for the _req parameter to indicate that it's not used in the function, which is a common convention.

Stateless Authentication: One of the advantages of JWT-based authentication is its statelessness. To log a user out, we simply need to:

  1. Remove the JWT token cookie from the client
  2. There's no need to invalidate anything on the server since the server doesn't keep track of sessions

clearCookie('token'): This Express method removes the specified cookie from the client by setting:

  • An empty value
  • An expiration date in the past

REST convention: We use the DELETE HTTP method for logout, following REST conventions where:

  • DELETE is used for removing resources
  • A session can be thought of as a resource that we're removing

Testing the Logout Route

Let's test the logout route to ensure it's working correctly:

fetch('/api/session', {
  method: 'DELETE',                            // DELETE method for logout
  headers: {
    "Content-Type": "application/json",
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`
  }
}).then(res => res.json()).then(data => console.log(data));

If everything is working correctly, you should see { message: 'success' } in the console, and the token cookie should disappear from your browser's cookies.

Verification Steps for Logout:

  1. Check the response: Should be { message: 'success' }
  2. Check cookies: In browser dev tools > Application > Cookies, the 'token' cookie should be gone
  3. Check authentication status: Try accessing a protected route or GET /api/session - should show not authenticated

Note: Even after logout, the XSRF-TOKEN cookie might still be present - this is normal and not a security issue, as it's only used for CSRF protection.

Committing and Merging the Logout Branch

Once you've confirmed that the logout functionality is working, commit your changes and merge them into the dev branch:

git add .
git commit -m "Add User logout backend endpoint"
git checkout dev
git pull origin dev
git merge logout
git push origin dev

Implementing Signup Functionality

Creating a Signup Feature Branch

Let's create a new feature branch for the signup functionality:

git checkout dev
git checkout -b signup

Implementing the Signup Route

Update the backend/routes/api/users.js file to include the signup route:

// backend/routes/api/users.js
const express = require('express');
const bcrypt = require('bcryptjs');              // For password hashing

const { setTokenCookie, requireAuth } = require('../../utils/auth');  // Auth utilities
const { User } = require('../../db/models');     // User model

const router = express.Router();

// Sign up
router.post('/', async (req, res) => {           // POST /api/users endpoint
  const { email, password, username } = req.body;  // Extract user data
  
  // Hash the password
  const hashedPassword = bcrypt.hashSync(password);  // Create secure hash
  
  // Create a new user
  const user = await User.create({ email, username, hashedPassword });  // Save to DB

  // Create a safe user object (without hashedPassword)
  const safeUser = {
    id: user.id,
    email: user.email,
    username: user.username,
  };

  // Set the JWT cookie
  await setTokenCookie(res, safeUser);           // Login the new user automatically

  // Return the user information
  return res.json({
    user: safeUser                              // Return user data
  });
});

module.exports = router;

Let's break down what this signup route does:

  1. It extracts the email, password, and username from the request body.
  2. It hashes the password using bcrypt for secure storage.
  3. It creates a new user in the database with the provided information.
  4. It creates a safe user object (without sensitive information like the hashed password).
  5. It calls setTokenCookie to set a JWT cookie containing the user's information.
  6. Finally, it returns a JSON response with the user's information.

Password Security: We use bcrypt.hashSync() to securely hash the user's password before storing it:

  • The function generates a random salt
  • It then performs the bcrypt hashing algorithm (multiple rounds of a modified Blowfish cipher)
  • The resulting hash includes the salt, so we don't need to store it separately
  • This approach protects against rainbow table attacks and ensures two users with the same password will have different hashes

User Creation and Validation: When we call User.create(), Sequelize will:

  1. Run all model validations we defined in Phase 3
  2. Reject the creation with a validation error if any checks fail
  3. Create the user record in the database if all validations pass

Auto-Login After Signup: By calling setTokenCookie, we're automatically logging in the user after successful registration, which provides a smoother user experience (no need to log in separately after signing up).

Testing the Signup Route

Let's test the signup route to ensure it's working correctly:

fetch('/api/users', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`
  },
  body: JSON.stringify({
    email: 'spidey@spider.man',                // New user email
    username: 'Spidey',                        // New username
    password: 'password'                       // Password
  })
}).then(res => res.json()).then(data => console.log(data));

If everything is working correctly, you should see a JSON response with the new user's information, and a token cookie should be set in your browser.

Now, try to test some validation errors by trying to create a user with an existing email or username:

fetch('/api/users', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`
  },
  body: JSON.stringify({
    email: 'demo@user.io',                     // This email already exists
    username: 'NewUser',
    password: 'password'
  })
}).then(res => res.json()).then(data => console.log(data));

You should receive a Sequelize validation error because the email is already in use.

Validation Error Handling: When trying to create a user with a duplicate email, Sequelize will throw a SequelizeUniqueConstraintError. Our error-handling middleware from Phase 2 will format this into a user-friendly response.

Expected error response:

{
  "title": "Validation error",
  "message": "Validation error",
  "errors": {
    "email": "email must be unique"
  },
  "stack": "..."  // (in development mode only)
}
                

Additional validations: The User model also enforces other validations we defined in Phase 3:

  • Username must be 4-30 characters and not an email
  • Email must be 3-256 characters and a valid format
  • Both username and email must be unique

Committing and Merging the Signup Branch

Once you've confirmed that the signup functionality is working, commit your changes and merge them into the dev branch:

git add .
git commit -m "Add User signup backend endpoint"
git checkout dev
git pull origin dev
git merge signup
git push origin dev

Implementing Get Session User Functionality

Creating a Get Session Feature Branch

Let's create a new feature branch for the get session user functionality:

git checkout dev
git checkout -b get-session

Implementing the Get Session User Route

Update the backend/routes/api/session.js file to include the get session user route:

// backend/routes/api/session.js
// ... existing code

// Restore session user
router.get('/', (req, res) => {                 // GET /api/session endpoint
  const { user } = req;                         // Get user from request object
  if (user) {
    const safeUser = {                          // Create safe user object
      id: user.id,
      email: user.email,
      username: user.username,
    };
    return res.json({
      user: safeUser                           // Return user data if logged in
    });
  } else return res.json({ user: null });       // Return null if not logged in
});

module.exports = router;

This route is quite simple. It checks if there's a user on the request object (which would have been set by the restoreUser middleware). If there is, it returns a safe version of the user object. If not, it returns { user: null }.

Frontend Integration: This endpoint is particularly useful for frontend applications to check authentication status when the page loads or after navigation. A common pattern is:

  1. Frontend makes a GET request to /api/session when the application initializes
  2. If response contains a user, the frontend knows the user is logged in
  3. If response is null, the frontend can show login/signup options

restoreUser Middleware: Remember that our API router uses the restoreUser middleware, which:

  • Reads the JWT token cookie
  • Verifies the token
  • Finds the corresponding user in the database
  • Sets req.user to the user object (or null if invalid/missing token)

This route simply returns that user information (or null) to the client.

Testing the Get Session User Route

Let's test the get session user route to ensure it's working correctly. First, make sure you're logged in by using the login route. Then, test the get session user route:

fetch('/api/session')
  .then(res => res.json())
  .then(data => console.log(data));

If you're logged in, you should see your user information. If you're not logged in, you should see { user: null }.

Testing Sequence: To fully test this route, try the following sequence:

  1. When logged out: Call GET /api/session → Should return { user: null }
  2. Log in: Call POST /api/session with valid credentials
  3. When logged in: Call GET /api/session → Should return your user object
  4. Log out: Call DELETE /api/session
  5. When logged out again: Call GET /api/session → Should return { user: null }

This sequence verifies that the session state changes correctly with login and logout actions.

Note: Since this is a simple GET request with no request body, you don't need to include the CSRF token in the request headers. CSRF protection primarily applies to state-changing requests (POST, PUT, DELETE, etc.).

Committing and Merging the Get Session Branch

Once you've confirmed that the get session user functionality is working, commit your changes and merge them into the dev branch:

git add .
git commit -m "Add a backend endpoint to get the current user session"
git checkout dev
git pull origin dev
git merge get-session
git push origin dev

Understanding the Authentication Flow

Now that we've implemented all the authentication routes, let's take a step back and understand how they work together to create a complete authentication system.

The Authentication Flow

The authentication flow in our application follows these steps:

  1. Signup: A new user provides their email, username, and password. The password is hashed, and a new user record is created in the database. A JWT containing the user's information is set as a cookie, and the user is considered logged in.
  2. Login: An existing user provides their credentials (username or email) and password. The application verifies these against the database. If valid, a JWT containing the user's information is set as a cookie, and the user is considered logged in.
  3. Session Restoration: On subsequent requests, the restoreUser middleware checks for a valid JWT cookie. If found, it retrieves the corresponding user from the database and attaches it to the request object, making it available to route handlers.
  4. Session Check: The GET /api/session route can be used to check if a user is currently logged in. It returns the user's information if they are, or null if they're not.
  5. Logout: When a user logs out, the JWT cookie is cleared, and the user is no longer considered logged in.
  6. Protected Routes: Routes that should only be accessible to logged-in users can use the requireAuth middleware, which checks if a user is attached to the request and returns an error if not.

Complete Authentication Flow Diagram:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Browser   │     │  API Router │     │Auth Utilities│     │  Database   │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │                   │
┌──────┴──────┐            │                   │                   │
│  Signup/    │            │                   │                   │
│   Login     │            │                   │                   │
└──────┬──────┘            │                   │                   │
       │                   │                   │                   │
       │ POST /api/users or│                   │                   │
       │ POST /api/session │                   │                   │
       │───────────────────>                   │                   │
       │                   │                   │                   │
       │                   │ Find/create user  │                   │
       │                   │───────────────────────────────────────>
       │                   │                   │                   │
       │                   │  User data        │                   │
       │                   │<───────────────────────────────────────
       │                   │                   │                   │
       │                   │ setTokenCookie()  │                   │
       │                   │────────────────────>                  │
       │                   │                   │                   │
       │                   │    JWT token      │                   │
       │                   │<────────────────────                  │
       │                   │                   │                   │
       │  Response + Cookie│                   │                   │
       │<───────────────────                   │                   │
       │                   │                   │                   │
┌──────┴──────┐            │                   │                   │
│ Subsequent  │            │                   │                   │
│  Requests   │            │                   │                   │
└──────┬──────┘            │                   │                   │
       │ Request + JWT Cookie                  │                   │
       │───────────────────>                   │                   │
       │                   │                   │                   │
       │                   │ restoreUser()     │                   │
       │                   │────────────────────>                  │
       │                   │                   │                   │
       │                   │   Verify token    │                   │
       │                   │         │                   │
       │                   │                   │                   │
       │                   │ Find user by ID   │                   │
       │                   │───────────────────────────────────────>
       │                   │                   │                   │
       │                   │  User data        │                   │
       │                   │<───────────────────────────────────────
       │                   │                   │                   │
       │                   │req.user = user    │                   │
       │                   │<────────────────────                  │
       │                   │                   │                   │
       │                   │ Route handler with│                   │
       │                   │ access to req.user│                   │
       │                   │                   │                   │
       │     Response      │                   │                   │
       │<───────────────────                   │                   │
       │                   │                   │                   │
┌──────┴──────┐            │                   │                   │
│  Logout     │            │                   │                   │
└──────┬──────┘            │                   │                   │
       │ DELETE /api/session                   │                   │
       │───────────────────>                   │                   │
       │                   │                   │                   │
       │                   │ Clear token cookie│                   │
       │                   │         │                   │
       │                   │                   │                   │
       │  Response + Clear │                   │                   │
       │  Cookie Instruction                   │                   │
       │<───────────────────                   │                   │
       │                   │                   │                   │
                

Authentication State Management: This implementation follows a stateless authentication pattern where:

  • The server doesn't store session information
  • All session state is contained in the JWT token stored as a cookie
  • The token contains the user ID, which is used to look up the user on each request

This approach scales well because there's no server-side session store to maintain.

JWT-based Authentication

Our authentication system uses JSON Web Tokens (JWT) to maintain user sessions. This approach has several advantages:

The JWT is stored in an HTTP-only cookie, which helps protect against cross-site scripting (XSS) attacks because JavaScript code can't access HTTP-only cookies.

JWT Structure: A JSON Web Token consists of three parts separated by dots:

header.payload.signature
                

1. Header: Contains the token type and signing algorithm

{
  "alg": "HS256",
  "typ": "JWT"
}
                

2. Payload: Contains the claims (data) - in our case, user information

{
  "data": {
    "id": 1,
    "email": "demo@user.io",
    "username": "Demo-lition"
  },
  "iat": 1645556568,
  "exp": 1646161368
}
                

3. Signature: Created by signing the encoded header and payload using the secret key

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
                

Security considerations with JWT:

  • Secret key protection: The JWT_SECRET must be kept secure as anyone with this key could forge tokens
  • Expiration: Our tokens expire after 1 week (configurable in .env)
  • HTTP-only cookies: Prevents client-side JavaScript from accessing the token
  • CSRF protection: We still need CSRF protection as JWT in cookies doesn't inherently protect against CSRF

Real-world Considerations

In a real-world application, you might want to add additional features to this basic authentication system:

These features would build upon the foundation we've established in this phase.

Advanced Authentication Features:

1. Email Verification:

  • Generate a unique token when a user signs up
  • Store the token and a flag indicating the user is unverified
  • Send an email with a verification link containing the token
  • Create an endpoint that verifies the token and marks the user as verified
  • Restrict certain actions until verification is complete

2. Password Reset:

  • Create an endpoint for requesting a password reset
  • Generate a time-limited token and store it with the user
  • Send an email with a reset link containing the token
  • Create an endpoint to verify the token and allow setting a new password

3. Multi-factor Authentication (MFA):

  • Allow users to enable MFA in their settings
  • Generate and store a secret key for each user
  • After password verification, require a code from an authenticator app or SMS
  • Provide backup codes for recovery

4. Token Revocation:

  • Maintain a blacklist of revoked tokens
  • Check this list during token verification
  • Allow users to manually revoke tokens (log out all devices)

Common Issues and Troubleshooting

CSRF Token Issues

One common issue is getting a CSRF token error when making POST, PUT, DELETE, or PATCH requests. This can happen if:

Always make sure to include the XSRF-TOKEN value in your request headers for non-GET requests:

fetch('/api/session', {
  method: 'POST',
  headers: {
    "Content-Type": "application/json",
    "XSRF-TOKEN": `<value of XSRF-TOKEN cookie>`  // Must match cookie value exactly
  },
  body: JSON.stringify({ /* request body */ })
});

CSRF Protection Explained:

  1. When your application loads, it gets a CSRF token from /api/csrf/restore
  2. This token is stored in two places:
    • As a cookie named 'XSRF-TOKEN'
    • In your application's JavaScript memory
  3. For any state-changing request (POST/PUT/DELETE), you include the token from memory in the 'XSRF-TOKEN' header
  4. The server compares the cookie value to the header value
  5. If they match, the request is allowed; if not, it's rejected

Why this works: Other websites can cause your browser to send a request to your API with your cookies (CSRF attack), but they cannot read your cookies or set custom headers. Therefore, they cannot include the matching XSRF-TOKEN header.

Debugging CSRF issues:

  1. Check browser dev tools → Application → Cookies → Ensure XSRF-TOKEN exists
  2. Verify the token in the header matches exactly (including any URL encoding)
  3. Try refreshing the token with a request to /api/csrf/restore

Authentication Issues

If you're having issues with authentication, check the following:

You can use console.log statements to debug these issues, or use the browser's developer tools to inspect cookies and network requests.

Authentication Debugging Steps:

1. Database issues:

# Check if user exists and password is correct
sqlite3 db/dev.db "SELECT id, username, email FROM Users WHERE username = 'Demo-lition';"
                

2. JWT cookie issues:

  • Check browser dev tools → Application → Cookies → Look for 'token' cookie
  • If missing, the setTokenCookie function might not be working

3. restoreUser middleware issues:

  • Add temporary logging to utils/auth.js to track JWT verification:
// In restoreUser middleware
console.log('Token from cookie:', token?.substring(0, 20) + '...');
try {
  // After verification
  console.log('Verified JWT payload:', jwtPayload);
  // After finding user
  console.log('Found user:', req.user ? req.user.username : 'null');
} catch (e) {
  console.error('JWT verification error:', e);
}
                

4. Common authentication errors:

  • JWT malformed: Token is corrupted or improperly formatted
  • Invalid signature: JWT_SECRET doesn't match the one used to create the token
  • JWT expired: Token has passed its expiration time

Database Issues

If you're having issues with the database, check the following:

You can use sqlite3 db/dev.db to directly inspect your database and verify that data is being stored correctly.

Database Command Cheat Sheet:

Check database schema:

sqlite3 db/dev.db ".schema Users"
                

List all users:

sqlite3 db/dev.db "SELECT id, username, email FROM Users;"
                

Check specific user:

sqlite3 db/dev.db "SELECT * FROM Users WHERE username = 'Demo-lition';"
                

Sequelize validation debugging:

If your model validations are failing, you can temporarily log the full error object:

router.post('/', async (req, res) => {
  try {
    // Attempt to create user
    const user = await User.create({...});
    // ...rest of handler
  } catch (err) {
    console.error('Validation error details:', err);
    if (err.name === 'SequelizeValidationError') {
      err.errors.forEach(e => console.log(`- ${e.path}: ${e.message}`));
    }
    // Let the error handling middleware handle it
    throw err;
  }
});
                

Next Steps

Now that you have implemented the core authentication routes for your application, you are ready to move on to Phase 5: Validating the Request Body. In that phase, you'll:

These validations will help improve the security and reliability of your application by ensuring that only valid data is processed.

Looking ahead to validation: In Phase 5, we'll enhance our routes with robust input validation that will:

  • Validate data before attempting database operations
  • Provide clear, descriptive error messages for invalid inputs
  • Implement consistent validation patterns across the application
  • Prevent common attack vectors like injection attacks

Validation vs. Model Constraints: While our Sequelize models already have some validations, adding validation at the route level provides several benefits:

  • More detailed error messages tailored to the user's input
  • Validation before accessing the database (better performance)
  • Additional validation logic not supported by the model (e.g., password strength)
  • Consistent error format across all routes

Continue to Phase 5: Validating the Request Body