User Authentication Level 4
Protecting Sensitive Data Using bcrypt, Passport.js, and Best Practices for Environment Variables
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 intoprocess.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
- 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.
- 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
Always use
.env
for sensitive data to avoid exposing it in your code.Add
.env
to your.gitignore
file to prevent accidental commits.Encrypt passwords to protect user credentials.
By following these practices, you can build secure and scalable applications that safeguard sensitive information.