In this lesson, we will use the middleware and cookies to create a user authentication system so that only authenticated users can access certain routes.
User registration
First of all, update the Prisma schema to include a password
field.
1model User {
2 id Int @id @default(autoincrement())
3 email String @unique
4 name String
5 password String
6
7 posts Post[]
8}
Apply this new schema by running the following command:
1npx prisma migrate dev
Update the interface for creating new users.
views/user/new.pug
1extends ../layout.pug
2
3block meta
4 name Create New User
5
6block content
7 form(id="newUser")
8 label(for="name") Name:
9 input(type="text", name="name", id="name")
10 br
11 br
12
13 label(for="email") Email:
14 input(type="email", name="email", id="email")
15 br
16 br
17
18 label(for="password") Password:
19 input(type="password", name="password", id="password")
20 br
21 br
22
23 input(type="submit", value="Submit")
24
25 script.
26 . . .
1document.addEventListener("DOMContentLoaded", function () {
2 const form = document.getElementById("newUser");
3 const nameInput = document.getElementById("name");
4 const emailInput = document.getElementById("email");
5 const passwordInput = document.getElementById("password");
6
7 const formData = new URLSearchParams();
8
9 nameInput.addEventListener("input", function () {
10 formData.set("name", nameInput.value);
11 });
12
13 emailInput.addEventListener("input", function () {
14 formData.set("email", emailInput.value);
15 });
16
17 passwordInput.addEventListener("input", function () {
18 formData.set("password", passwordInput.value);
19 });
20
21 form.addEventListener("submit", async function (event) {
22 event.preventDefault(); // Prevent the default form submission
23
24 await fetch("/users/new", {
25 method: "POST",
26 body: formData.toString(),
27 headers: {
28 "Content-Type": "application/x-www-form-urlencoded",
29 },
30 }).then((data) => (window.location.href = data.url));
31 });
32});
As well as the corresponding controller:
controllers/userController.js
1import prisma from "../libs/prisma.js";
2
3const userController = {
4 . . .
5
6 create: async function (req, res) {
7 const { name, email, password } = req.body;
8
9 const user = await prisma.user.create({
10 data: {
11 name: name,
12 email: email,
13 password: password,
14 },
15 });
16
17 res.redirect(`/users/${user.id}`);
18 },
19
20 signin_form: async function (req, res) {
21 res.render("user/signin/show");
22 },
23
24 . . .
25};
26
27export default userController;
These changes ensure the user provides a password when creating a new user account.
User authentication
Next, we need to set up a mechanism that allows the user to sign in. This works similarly to user registration, except the controller will be in charge of matching the provided password with the one stored in the database.
To begin with, you'll need a sign-in page.
1import prisma from "../libs/prisma.js";
2
3const userController = {
4 . . .
5 signin_form: async function (req, res) {
6 res.render("user/signin/show");
7 },
8 . . .
9};
10
11export default userController;
This signin_form
controller method will be in charge of rendering the sign-in page, which looks like this:
views/user/signin/show.pug
1extends ../../layout.pug
2
3block meta
4 name Sign In
5
6block content
7 form(id="signIn")
8 label(for="email") Email:
9 input(type="email", name="email", id="email")
10 br
11 br
12
13 label(for="password") Password:
14 input(type="password", name="password", id="password")
15 br
16 br
17
18 input(type="submit", value="Submit")
19
20 script.
21 . . .
1document.addEventListener("DOMContentLoaded", function () {
2 const form = document.getElementById("signIn");
3 const emailInput = document.getElementById("email");
4 const passwordInput = document.getElementById("password");
5
6 const formData = new URLSearchParams();
7
8 emailInput.addEventListener("input", function () {
9 formData.set("email", emailInput.value);
10 });
11
12 passwordInput.addEventListener("input", function () {
13 formData.set("password", passwordInput.value);
14 });
15
16 form.addEventListener("submit", async function (event) {
17 event.preventDefault(); // Prevent the default form submission
18
19 await fetch("/users/signin", {
20 method: "POST",
21 body: formData.toString(),
22 headers: {
23 "Content-Type": "application/x-www-form-urlencoded",
24 },
25 }).then((data) => (window.location.href = data.url));
26 });
27});
Of course, don't forget to change the routers.
routes/user.js
1userRouter
2 .route("/signin")
3 .get(userController.signin_form)
4 .post(userController.signin);
When the user submits the sign-in form, a POST
request will be sent to /users/signin
, which will be handled by the signin()
controller method.
1import prisma from "../libs/prisma.js";
2
3const userController = {
4 . . .
5
6 signin_form: async function (req, res) {
7 res.render("user/signin/show");
8 },
9
10 signin: async function (req, res) {
11 const { email, password } = req.body;
12
13 const user = await prisma.user.findUnique({
14 where: {
15 email: String(email),
16 },
17 });
18
19 if (user.password === password) {
20 res
21 .cookie("authenticated", true, {
22 expires: new Date(Date.now() + 12 * 30 * 24 * 3600), // cookie will be removed after 1 year
23 })
24 .redirect("/users/signin/success");
25 } else {
26 res.redirect("/users/signin/failure");
27 }
28 },
29
30 . . .
31};
32
33export default userController;
If the provided password matches the one stored in the database, the user is successfully authenticated and will be redirected to /users/signin/success
. If not, the user will be redirected to /users/signin/failure
.
Also notice that we are sending a cookie with the response when the user is successfully authenticated.
1res
2 .cookie("authenticated", true, {
3 expires: new Date(Date.now() + 12 * 30 * 24 * 3600), // cookie will be removed after 1 year
4 })
5 .redirect("/users/signin/success");
In this example, we set the cookie name to be authenticated
, and the value is true
. The cookie is set to expire after one year.
This is how you can work with cookies on the server side. The cookie will be transferred to the client side, and will be automatically captured by the browser, without having to do anything.
And lastly, don't forget the corresponding success and failure pages.
routes/user.js
1userRouter.route("/signin/success").get(userController.success);
2
3userRouter.route("/signin/failure").get(userController.failure);
controllers/userController.js
1import prisma from "../libs/prisma.js";
2
3const userController = {
4 . . .
5
6 signin_form: async function (req, res) {
7 res.render("user/signin/show");
8 },
9
10 signin: async function (req, res) {
11 const { email, password } = req.body;
12
13 const user = await prisma.user.findUnique({
14 where: {
15 email: String(email),
16 },
17 });
18
19 if (user.password === password) {
20 res
21 .cookie("authenticated", true, {
22 expires: new Date(Date.now() + 12 * 30 * 24 * 3600), // cookie will be removed after 1 year
23 })
24 .redirect("/users/signin/success");
25 } else {
26 res.redirect("/users/signin/failure");
27 }
28 },
29
30 success: async function (req, res) {
31 res.render("user/signin/success");
32 },
33
34 failure: async function (req, res) {
35 res.render("user/signin/failure");
36 },
37
38 . . .
39};
40
41export default userController;
views/user/signin/success.pug
1extends ../../layout.pug
2
3block meta
4 name Success
5
6block content
7 p You've been signed in.
views/user/signin/failure.pug
1extends ../../layout.pug
2
3block meta
4 name Failure
5
6block content
7 p Something went wrong.
User authorization
Finally, you should ensure that only authenticated users can access certain routes. For example, the /posts/new
route should be protected, as you don't want any random user to spam your site. To do this, we must install another package, cookie-parser
.
1npm install cookie-parser
index.js
1import cookieParser from "cookie-parser";
2
3const app = express();
4app.use(cookieParser());
5
6. . .
This package allows you to access and parse the cookies directly from the server side. The parsed cookies will be converted into an object, which means to access the authenticated
cookie we created previously, you can use the dot operator (.
).
middlewares/auth.js
1export default async function isAuthenticated(req, res, next) {
2 if (req.cookies.authenticated === "true") {
3 next();
4 } else {
5 res.redirect("/users/signin");
6 }
7}
In this example middleware, if the authenticated
cookie is found, and its value equals true
, the user is considered authenticated, and they will be taken to the next route handler. If not, the user will be redirected to the sign in page.
Next, use this middleware on the routes that you wish to be protected.
routes/post.js
1import isAuthenticated from "../middlewares/auth.js";
2
3postRouter
4 .route("/new")
5 .get(isAuthenticated, postController.new) // <== Only authenticated users can add new post
6 .post(upload.single("image"), postController.create);
It works, but...
There are several security issues with our setup. For instance, the passwords are directly stored in the database, but in practice, they should be encrypted. Also, our authentication process relies entirely on cookies, which means if the user creates their own authenticated
cookie, they will be able to bypass our authentication.
A production-ready user authentication system is not as easy as people might think, so in practice, it is recommended to use a third-party package instead, which will have a lot of people backing it up, a lot more features, and fewer security risks.
This lesson also marks the end of this chapter, where we systematically discuss how JavaScript works in both the frontend and the backend. After this, we are only going to cover some miscellaneous topics, feel free to jump to the next chapter if you are not interested.