Lesson 5: Learn Express Authentication
Learn to authenticate users with JSON Web Tokens (JWT) and create custom authentication middleware
What is authentication on the backend?
What are JSON Web Tokens? (JWT)
The structure of a JWT
JWT debugger
Let’s setup jsonwebtoken in our server
Now we will build a generateToken function for creating new JWT’s that we can pass back to the user on login for authentication at the bottom of usersRouter.js
Build a generateToken function for JWT’s
// Function to generate a JSON Web Token (JWT) for a given user
function generateToken(user) {
// Define the payload to be included in the token, containing user data
const payload = {
id: user.id,
username: user.username,
admin: user.admin,
};
// Get the JWT secret from an environment variable, or use a default value
const secret = process.env.JWT_SECRET || "Satoshi Nakamoto";
// Define the options for the JWT, including the token expiration time
const options = {
expiresIn: "1d",
};
// Generate and return the JWT using the payload, secret, and options
return jwt.sign(payload, secret, options);
}
Storing secrets on our server with environment variables
Create a .env file at the root of our project
# secret variable for the port of our server (used in index.js)
PORT=5501
# secret variable for our JWT secret (used in usersRouter.js)
JWT_SECRET=keepitsecretkeepitsafu
Accessing environment variables with dotenv
dotenv is a package that allows us to load environment variables from a .env file.
Update index.js to load “PORT” env variable
const dotenv = require("dotenv")
dotenv.config()
// Set the server to listen on the provided port, or 5500 if no port is specified
const PORT = process.env.PORT || 5500;
server.listen(PORT, () => {
// Log a message to the console when the server starts listening
console.log(`Server listening on port ${PORT}`);
});
Import dotenv at the top of your index.js
Initialize your environment variables with dotenv.config() right below your imports in index.js
Now your port should be 5501 when you restart the server
Using bcryptjs to finish our JWT setup
What is bcryptjs and why are we using it?
Now lets update our /login endpoint to use bcryptjs and our new generateToken function
Time to do some hashing! �`Brrrrr`
router.post("/login", (req, res) => {
// Extract the username and password from the request body
const { username, password } = req.body;
// Placeholder user object - later we will fetch the real user from the database
const DBuser = {
username: "test",
password: "pass1",
};
// Hash the password from the request body using bcrypt
// Later we will compare this hash to the hash stored in the database
// But for now, we will just do it manually
const hashedPassword = bcrypt.hashSync(DBuser.password, 14);
// Check if the user exists and the password matches using bcrypt
if (DBuser && bcrypt.compareSync(password, hashedPassword)) {
// Generate a JSON Web Token (JWT) for the user
const token = generateToken(DBuser);
// Send a success response with the JWT and user data
res
.status(200)
.json({ message: `Welcome ${DBuser.username}!`, token, DBuser });
} else {
// Send an error response if the credentials are invalid
res.status(401).json({ message: "Invalid credentials" });
}
});
Considering auth / access for the pleb wallet backend
For any backend we build it’s important to ask ourselves some basic questions around security / access to our server:
Who will have access to our server and what resource are we hosting
What resources are we hosting:
Who will have access and what access will they have:
How do we implement this authentication?
In general our API endpoints are the gateways into our backend, basically any functionality or logic that can be executed from the users side will pass through endpoints we open up.��And how do we intercept logic in between the request/response of our server?�Middleware!
Authentication middleware
We need to build an auth middleware for each unique permission set that our server will handle
This will include:
Example of custom middleware in Express:
// Define the middleware function
const myMiddleware = (req, res, next) => {
// Perform some checks or modifications to the request or response objects
// For example, you could add a custom header to the response:
res.setHeader('X-Custom-Header', 'Hello, world!');
// Call the next middleware function in the chain
next();
};
// Use the middleware function on a specific endpoint
app.get('/my-endpoint', myMiddleware, (req, res) => {
// Handle the request as normal
res.send('Hello, world!');
});
Define a middleware function with the correct parameters (req, res, next)
Place our new middleware function in between the route and the (req, res) parameters
What will our two auth middlewares do?
Create middleware directory / authenticate.js
Create authenticate.js middleware
const jwt = require("jsonwebtoken");
// Exporting a middleware function that takes the arguments req, res, and next
module.exports = (req, res, next) => {
// Extracting the token from the Authorization header of the request
const token = req.headers.authorization;
// Extracting the secret used to sign the JWT from an environment variable or using a default value
const secret = process.env.JWT_SECRET || "Satoshi Nakamoto";
// Checking if a token was provided in the request header
if (token) {
// Verifying the token using the provided secret
jwt.verify(token, secret, (err, decodedToken) => {
if (err) {
// If the token is not verified, return a status code of 401 and an error message
res.status(401).json({ message: "Not Allowed", Error: err });
} else {
// If the token is verified, execute the next middleware or endpoint logic
next();
}
});
} else {
// If no token was provided, return a status code of 401 and a message
res.status(401).json({ message: "No token!" });
}
};
Add authenticate middleware to createInvoice
const authenticate = require("../routers/middleware/authenticate");
…
// POST required info to create an invoice
router.post("/createInvoice", authenticate, (req, res) => {
const { value, memo } = req.body;
console.log(value, memo);
res.status(200).json({ message: "I'm alive!" });
});
Testing the authenticate middleware
We can now call our createInvoice endpoint with no authentication��We should be denied with a 401��“No token!”
Getting authentication from the API
Now let’s make a post request to /login with our hardcoded DBuser credentials to get a JWT authentication token
Testing an authenticated request to /createInvoice
Take the token from the successful request to /login and add it as a header with the key “authorization”
Create your authenticateAdmin middleware
Almost there!
Create authenticateAdmin.js part #1
const jwt = require("jsonwebtoken");
module.exports = (req, res, next) => {
// Extracting the token from the request header
const token = req.headers.authorization;
// Setting up the JWT secret for token verification
const secret = process.env.JWT_SECRET || "Satoshi Nakamoto";
// If token is present, attempt to verify it using the JWT module
if (token) {
jwt.verify(token, secret, async (err, decodedToken) => {
// If token is not verified, return 401 error
if (err || !decodedToken) {
res.status(401).json({ message: "Error with your verification" });
} else {
Create authenticateAdmin.js part #2
// If token is verified, find the user using their username from the database
// Placeholder user object - later we will fetch the real user from the database
const user = {
username: "test",
password: "pass1",
adminKey: 1234,
};
// Extracting admin key from user object if it exists
const adminKey = user?.adminKey?.toString() ?? "";
// Checking if extracted admin key matches with the one in env variables
if (adminKey !== process.env.ADMIN_KEY) {
// If admin key does not match, return 401 error
res.status(401).json({ message: "Must be an admin" });
} else {
// If admin key matches, let the endpoint continue executing
next();
}
}
});
} else {
// If no token is present, return 401 error
res.status(401).json({ message: "No token!" });
}
};
Add the new ADMIN_KEY env variable to .env
# secret variable for the port of our server (used in index.js)
PORT=5501
# secret variable for our JWT secret (used in usersRouter.js)
SECRET=keepitsecretkeepitsafu
# secret variable for admin key (used in authenticateAdmin.js)
ADMIN_KEY=1234
Add authenticateAdmin to payInvoice endpoint
const authenticateAdmin = require("../routers/middleware/authenticateAdmin");
…
// POST an invoice to pay
router.post("/payInvoice", authenticateAdmin, (req, res) => {
const { payment_request } = req.body;
console.log(payment_request);
res.status(200).json({ message: "I'm alive!" });
});
Testing the authenticateAdmin middleware
We can now call our payInvoice endpoint with no authentication��We should be denied with a 401��“No token!”
Testing an authenticated request to /payInvoice
Review
Our server authentication at a high level
Develop and apply JWT, verify user identities, and regulate permissions using middleware.
Resources