User Authentication Level 4

Protecting Sensitive Data Using bcrypt, Passport.js, and Best Practices for Environment Variables

User Authentication Level 4

User authentication is essential for protecting sensitive user data and ensuring secure access to resources. This Level 4 user authentication example uses bcrypt, Passport.js, and PostgreSQL while leveraging environment variables for enhanced security.


Why Use Environment Variables?

Environment variables are key-value pairs stored outside your application's code. They are used to securely store sensitive data like database credentials, API keys, and secret keys. These values are loaded into your application at runtime.

Risks of Not Storing Secrets Properly

If sensitive information is hardcoded in your application or committed to a repository, it may expose your application to:

  • Data breaches: Attackers can access your keys from version control history or the codebase.

  • Configuration leaks: Exposing sensitive data to unauthorized personnel.

  • Production risks: Hardcoding secrets ties them to specific environments, complicating deployments and scaling.


Code Explanation: Secure Authentication Example

1. Setting Up the Environment Variables

The .env file contains sensitive configuration values:

SESSION_SECRET="TOPSECRET"
PG_USER="postgres"
PG_HOST="localhost"
PG_DATABASE="secrets"
PG_PASSWORD="postgres"
PG_PORT="5432"

2. Ignoring Sensitive Files

A .gitignore file is essential to prevent committing sensitive files like .env to version control:

node_modules/
.env

This ensures no sensitive data is exposed when pushing code to platforms like GitHub.


3. Code Walkthrough

Below is a detailed explanation of the provided index.js file:

a. Imports and Initialization

import express from "express";
import bodyParser from "body-parser";
import pg from "pg";
import bcrypt from "bcrypt";
import passport from "passport";
import { Strategy } from "passport-local";
import session from "express-session";
import env from "dotenv";
  • dotenv: Loads .env file contents into process.env.

  • express-session: Enables session management.

  • passport-local: Configures local strategy for Passport.js.

env.config();

env.config() initializes environment variables.


b. Session and Middleware Configuration

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
  })
);
  • SESSION_SECRET: Stored in .env, this key ensures sessions are securely signed.

  • resave: false: Prevents resaving unchanged sessions.

  • saveUninitialized: true: Allows saving uninitialized sessions.

app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static("public"));
app.use(passport.initialize());
app.use(passport.session());

These middlewares enable request parsing, static file serving, and session handling.


c. Connecting to the Database

const db = new pg.Client({
  user: process.env.PG_USER,
  host: process.env.PG_HOST,
  database: process.env.PG_DATABASE,
  password: process.env.PG_PASSWORD,
  port: process.env.PG_PORT,
});
db.connect();

Database credentials are securely retrieved from environment variables, ensuring they're not exposed in the codebase.


d. Authentication Routes

  1. Home and Login Pages
app.get("/", (req, res) => res.render("home.ejs"));
app.get("/login", (req, res) => res.render("login.ejs"));
app.get("/register", (req, res) => res.render("register.ejs"));

These routes render the homepage, login, and registration forms.

  1. Secrets Page
app.get("/secrets", (req, res) => {
  if (req.isAuthenticated()) {
    res.render("secrets.ejs");
  } else {
    res.redirect("/login");
  }
});
  • req.isAuthenticated(): Ensures only authenticated users can access the secrets page.

e. User Registration

app.post("/register", async (req, res) => {
  const email = req.body.username;
  const password = req.body.password;

  try {
    const checkResult = await db.query("SELECT * FROM users WHERE email = $1", [email]);
    if (checkResult.rows.length > 0) {
      res.redirect("/login");
    } else {
      bcrypt.hash(password, saltRounds, async (err, hash) => {
        if (err) {
          console.error("Error hashing password:", err);
        } else {
          const result = await db.query(
            "INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *",
            [email, hash]
          );
          const user = result.rows[0];
          req.login(user, (err) => res.redirect("/secrets"));
        }
      });
    }
  } catch (err) {
    console.error(err);
  }
});
  • Password hashing: Protects user passwords using bcrypt.

  • Check for existing users: Ensures duplicate accounts aren’t created.


f. User Login

passport.use(
  new Strategy(async function verify(username, password, cb) {
    try {
      const result = await db.query("SELECT * FROM users WHERE email = $1 ", [username]);
      if (result.rows.length > 0) {
        const user = result.rows[0];
        const storedHashedPassword = user.password;
        bcrypt.compare(password, storedHashedPassword, (err, valid) => {
          if (err) return cb(err);
          if (valid) return cb(null, user);
          return cb(null, false);
        });
      } else {
        return cb("User not found");
      }
    } catch (err) {
      console.error(err);
    }
  })
);
  • bcrypt.compare: Verifies the hashed password matches the user input.

  • passport.authenticate: Handles authentication logic.


g. Sessions with Passport.js

passport.serializeUser((user, cb) => cb(null, user));
passport.deserializeUser((user, cb) => cb(null, user));
  • Serialization: Saves user data to the session.

  • Deserialization: Retrieves user data from the session for future requests.


4. Logout Implementation

app.get("/logout", (req, res) => {
  req.logout(function (err) {
    if (err) return next(err);
    res.redirect("/");
  });
});

req.logout() destroys the user session, logging the user out.


Conclusion

This Level 4 user authentication implementation demonstrates secure practices such as:

  • Password hashing with bcrypt.

  • Secure session management using environment variables.

  • Database credential management via .env.

Key Takeaways

  1. Always use .env for sensitive data to avoid exposing it in your code.

  2. Add .env to your .gitignore file to prevent accidental commits.

  3. Encrypt passwords to protect user credentials.

By following these practices, you can build secure and scalable applications that safeguard sensitive information.