Learnwizy Technologies Logo

Learnwizy Technologies

Class 32: User Authentication & Authorization (Basics)

Building web applications often involves managing users and controlling access to certain features or data. This requires understanding two crucial concepts: Authentication and Authorization.


Authentication vs. Authorization

Authentication vs Authorization

Common Authentication Methods


Secure Password Storage

Never store plaintext passwords in your database! If your database is compromised, all user passwords would be exposed, leading to severe security breaches.

Password Hashing with Salt

Introduction to JSON Web Tokens (JWT)

A JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed.

JWT Structure
npm install jsonwebtoken

Implementing User Registration (Signup)

We'll create a simple user model and an endpoint for user registration.

1. User Model (models/User.js):

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true,
    match: [/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/, 'Please enter a valid email address']
  },
  password: {
    type: String,
    required: true,
    minlength: [6, 'Password must be at least 6 characters long']
  },
  role: { // For authorization later
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  }
}, {
  timestamps: true
});

// Hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) { // Only hash if password was modified
    next();
  }
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Method to compare entered password with hashed password
userSchema.methods.matchPassword = async function(enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

2. Registration Endpoint (server.js):

// server.js (main file)
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/db');
const User = require('./models/User'); // Import User model

const app = express();
const port = process.env.PORT || 3000;

connectDB();
app.use(express.json());

// POST /api/register - User Registration
app.post('/api/register', async (req, res) => {
  const { username, email, password } = req.body;

  try {
    // Check if user already exists
    const userExists = await User.findOne({ email });
    if (userExists) {
      return res.status(400).json({ message: 'User with that email already exists.' });
    }

    // Create new user (password hashing handled by pre-save hook in User model)
    const user = await User.create({
      username,
      email,
      password,
    });

    res.status(201).json({
      message: 'User registered successfully!',
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });

  } catch (error) {
    if (error.name === 'ValidationError') {
      const messages = Object.values(error.errors).map(val => val.message);
      return res.status(400).json({ message: 'Validation failed', errors: messages });
    }
    console.error('Error during registration:', error);
    res.status(500).json({ message: 'Internal server error.' });
  }
});

// ... other routes and app.listen() ...

Test with Postman/Insomnia:


Implementing User Login (Signin)

After registration, users need to log in. Upon successful login, we'll generate and send a JWT.

1. JWT Generation Function:

Add a method to your User model to generate a JWT.

// models/User.js (add this method to userSchema)
const jwt = require('jsonwebtoken'); // npm install jsonwebtoken

userSchema.methods.getSignedJwtToken = function() {
  return jwt.sign({ id: this._id, role: this.role }, process.env.JWT_SECRET, {
    expiresIn: process.env.JWT_EXPIRE
  });
};

Add these to your .env file:

# .env
JWT_SECRET=supersecretjwtkeythatshouldbeverylongandrandom
JWT_EXPIRE=1h # e.g., 1 hour

2. Login Endpoint (server.js):

// server.js (main file)
// ... (imports and setup) ...
const jwt = require('jsonwebtoken'); // Import jsonwebtoken

// POST /api/login - User Login
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;

  // Basic validation
  if (!email || !password) {
    return res.status(400).json({ message: 'Please enter email and password.' });
  }

  try {
    // Check for user by email
    const user = await User.findOne({ email }).select('+password'); // Select password explicitly

    if (!user) {
      return res.status(401).json({ message: 'Invalid credentials.' });
    }

    // Check if password matches
    const isMatch = await user.matchPassword(password);

    if (!isMatch) {
      return res.status(401).json({ message: 'Invalid credentials.' });
    }

    // If credentials match, generate JWT
    const token = user.getSignedJwtToken();

    res.status(200).json({
      success: true,
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });

  } catch (error) {
    console.error('Error during login:', error);
    res.status(500).json({ message: 'Internal server error.' });
  }
});

// ... other routes and app.listen() ...

Test with Postman/Insomnia:


Protecting Routes with JWT (Basic Middleware)

Now that users can get a JWT, we need a way to protect certain API routes so that only authenticated users can access them. We'll create an Express middleware for this.

1. Authentication Middleware (middleware/auth.js):

// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User'); // Import your User model

const protect = async (req, res, next) => {
  let token;

  // Check if Authorization header exists and starts with 'Bearer'
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
    // Extract the token
    token = req.headers.authorization.split(' ')[1];
  }

  // Make sure token exists
  if (!token) {
    return res.status(401).json({ message: 'Not authorized to access this route. No token provided.' });
  }

  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    console.log('Decoded JWT:', decoded);

    // Attach user to the request object (excluding password)
    req.user = await User.findById(decoded.id).select('-password');

    if (!req.user) {
        return res.status(401).json({ message: 'Not authorized, user not found.' });
    }

    next(); // Proceed to the next middleware/route handler
  } catch (error) {
    console.error('Token verification failed:', error);
    return res.status(403).json({ message: 'Not authorized, token failed or expired.' });
  }
};

module.exports = protect;

2. Protecting a Route (server.js):

// server.js (main file)
// ... (imports and setup) ...
const protect = require('./middleware/auth'); // Import the protect middleware

// Example protected route: Get user profile
app.get('/api/profile', protect, async (req, res) => {
  // If we reach here, the user is authenticated, and req.user contains their data
  res.status(200).json({
    success: true,
    message: 'Welcome to your profile!',
    user: req.user
  });
});

// ... other routes and app.listen() ...

Test with Postman/Insomnia:

  1. First, perform a login request to get a JWT. Copy the token.
  2. Method: GET
  3. URL: http://localhost:3000/api/profile
  4. Headers: Add an Authorization header with value Bearer YOUR_JWT_TOKEN (replace YOUR_JWT_TOKEN with the token you copied).
  5. Send the request. You should get a 200 OK response with the user's profile.
  6. Try sending the request without the Authorization header, or with an invalid token, to see the 401 Unauthorized or 403 Forbidden responses.

You've now implemented basic user registration, login, and route protection using JWTs. This forms the backbone of secure user management in your applications. In the next class, we'll explore more advanced authentication topics, including refresh tokens and role-based access control.