DEV Community

Cover image for Implementing Passport Local With Yarn, Typescript, Express and PostgreSQL
Ethan
Ethan

Posted on • Edited on • Originally published at ethan-dev.com

Implementing Passport Local With Yarn, Typescript, Express and PostgreSQL

Introduction

Hello! 😎

In this tutorial I will show you how to implement passport local into a new Nodejs project. This tutorial will use PostgreSQL for the database but feel free to change it to a database of your choice. ☺️


Requirements

  • Basic knowledge of Nodejs
  • PostgreSQL installed

Initialize The Project

First we need to initialize the project, change the current directory to somewhere of your choice and run the following command to initialize the project:



yarn init -y


Enter fullscreen mode Exit fullscreen mode

This will create a simple package.json file in the current directory. Next we will need to install the dependencies that will be used for this project, to install them run the following command:



yarn add express bcrypt body-parser connect-pg-simple dotenv express-session passport passport-local pg


Enter fullscreen mode Exit fullscreen mode

The above command will install the dependencies needed to run the project, since this project will be using TypeScript, we will need to install the type files etc. To ensure they are only installed for a development environment we will use yarn with the "-D" flag, as follows:



yarn add --dev @types/connect-pg-simple @types/pg @types/bcrypt @types/express @types/express-session @types/node @types/passport @types/passport-local typescript


Enter fullscreen mode Exit fullscreen mode

Now that the dependencies have been taken care of we need to create a "tsconfig" file to handle converting TypeScript into JavaScript, to initialize this file simply run the following command:



npx tsc --init


Enter fullscreen mode Exit fullscreen mode

Once the file is created, open it up and populate it with the following config:



{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "esModuleInterop": true,
        "strict": true,
        "outDir": "./dist"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
}


Enter fullscreen mode Exit fullscreen mode

The above is nothing too complicated, it initializes strict mode and converts all files under the src directory into JavaScript files outputted to the dist directory. Pretty basic configuration but feel free to customize it to your needs.

Now that the project has been initialized we can now create the sql files that will be used in our PostgreSQL database. πŸ‘€


Configuring The Database

Next up we need to create the tables that will be used for the database, if you have not done so yet create a new database in PostgreSQL, this can be done in the psql shell with the following command:



create database app;


Enter fullscreen mode Exit fullscreen mode

This will create a new database called "app".

The first table we will create will handle the express sessions information, create a new directory in the current directory called "sql" and create a new sql file called "sql/session.sql" and populate it with the following:



CREATE TABLE session(
    sid VARCHAR(255) NOT NULL COLLATE "default",
    sess json NOT NULL,
    expire TIMESTAMP(6) NOT NULL
)

WITH (OIDS=FALSE);
ALTER TABLE session ADD CONSTRAINT session_pkey PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE;


Enter fullscreen mode Exit fullscreen mode

This file will create a new table to handle the express sessions we will be implementing later.

We also need another table to handle users that will authenticate using our local passport strategy, create a new file called "sql/user.sql" and populate it with the following:



CREATE SEQUENCE users_id_seq;

CREATE TABLE users (
    id INTEGER NOT NULL DEFAULT nextval('users_id_seq'),
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE (email)
);

ALTER SEQUENCE users_id_seq OWNED BY users.id;

CREATE INDEX ON users (email);


Enter fullscreen mode Exit fullscreen mode

The above will create a table to handle users, we also want the email to be unique so another user cannot register with the same email address.

To create the tables enter the psql shell with the following command and then run \i with the location to the above sql files like so:



psql -d app

\i sql/session.sql
\i sql/user.sql


Enter fullscreen mode Exit fullscreen mode

This should create both of the tables without any errors.

Now that the database is taken care of we can now start writing the code for our application.


Writing The Database Code

First off we will write the code for connecting and interacting with the PostgreSQL database. Create a new directory called "src/db". Next create a file called "src/db/psql.ts" and add the following imports:



import { Pool } from 'pg';
import session from 'express-session';
import pgSession from 'connect-pg-simple';
import bcrypt from 'bcrypt';


Enter fullscreen mode Exit fullscreen mode

Next to initialize the PostgreSQL pool and the session store add the following below the above imports:



const pgSessionStore = pgSession(session);

const pool = new Pool({
    connectionString: process.env.DATABASE_URL
});


Enter fullscreen mode Exit fullscreen mode

The DATABASE_URL is a reference to the psql link that will be used to connect to our database, don't worry about it too much now.

Next we will create a function to test the connection to the database because if we can't connect, there isn't much point for the application to run:



const testConnection = (): Promise<void> => {
    return new Promise((resolve, reject) => {
        pool.connect((error, client, release) => {
            if (error || !client) {
                reject(new Error('Failed to connect to the database'));
            }

            release();
            resolve();
        })
    });   
};


Enter fullscreen mode Exit fullscreen mode

The above code tests if we can connect to the PostgreSQL database, if the connection is ok it resolves, if the connection is not ok it will reject with a connection failure error.

Next we need a function to insert a new user into the database, this will be done when the user has registered with the application. The code for this is simply as follows:



const insertUser = async (email: string, password: string): Promise<void> => {
        const hashedPassword = await bcrypt.hash(password, 10);

        await pool.query('INSERT INTO users (email, password) VALUES ($1, $2)', [email, hashedPassword]);
};


Enter fullscreen mode Exit fullscreen mode

Always make sure to hash the passwords before inserting the data into the database, pretty small but powerful function.

Next we will need a function to authenticate the user, in this case via email:



const authenticateUserByEmail = async (email: string, password: string): Promise<Express.User | Error> => {
    const res = await pool.query('SELECT * FROM users WHERE email = $1', [email]);

    if (res.rows.length) {
        const user = res.rows[0];
        const match = await bcrypt.compare(password, user.password);

        if (match) {
            return user;
        } else {
            throw new Error('Incorrect email and/or password');
        }
    } else {
        throw new Error('User not found');
    }
};


Enter fullscreen mode Exit fullscreen mode

The above checks if the user actually exists in the database and if so makes sure the passwords match up, if everything is ok the user is returned, if something went wrong an error is thrown. Such as is the user is not found or the passwords don't match up.

The final function we need is one that will help deserialize the user, this function is also pretty straight forward:



const deserializeUserById = async (id: number): Promise<Express.User | Error> => {
    const res = await pool.query('SELECT * FROM users WHERE id = $1', [id]);

    if (res.rows.length) {
        return res.rows[0];
    } else {
        throw new Error('User was not found');
    }
};


Enter fullscreen mode Exit fullscreen mode

If the user exists return the user, if they don't exist throw an error. This will be explained in more detail later on.

Finally we need to export the needed variables and functions:



export {
    pgSessionStore,
    pool,
    testConnection,
    insertUser,
    authenticateUserByEmail,
    deserializeUserById
};


Enter fullscreen mode Exit fullscreen mode

If you're like me and like to have an "index.ts" file in each directory you can create a new file called "src/db/index.ts" and have it export what is needed:



export {
    pgSessionStore,
    pool,
    testConnection,
    insertUser,
    authenticateUserByEmail,
    deserializeUserById
} from './psql';


Enter fullscreen mode Exit fullscreen mode

Exporting what is needed from an index file also means you only need to import one file to include everything which is neat.

Thats the code to handle the database completed, next up we will implement the code needed for passport. πŸ˜„


Writing The Passport Code

Next we will write the code for the passport module that will alway us to authenticate the user.

Create a new directory to handle the routes and the authentication side of things using the following command:



mkdir -p src/routes/auth/passport


Enter fullscreen mode Exit fullscreen mode

Create a new file called "src/routes/auth/passport/passport.ts" and add the following imports:



import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { authenticateUserByEmail, deserializeUserById } from './../../../db';


Enter fullscreen mode Exit fullscreen mode

The above imports the needed modules for this file including some functions from the file we created in the previous section.

The first thing we will implement is the local strategy that will be used to authenticate the users:



passport.use(new LocalStrategy({
        usernameField: 'email',
        passwordField: 'password'
    },
    async (email, password, done) => {
        console.log('local');
        try {
            const user = await authenticateUserByEmail(email, password);

            return done(null, (user as Express.User));
        } catch (error) {
            return done(error);
        }
    }
));


Enter fullscreen mode Exit fullscreen mode

-
Since we will using email and not username we specify email in the usernameField. After that we simple authenticate the user, if they exist and the passwords match. If ok the user will be logged in, if not ok an error will be returned.

Finally we need two functions, one to serialize the user and one to deserialize the user, nothing too complicated the functions are simply the following:



passport.serializeUser((user: Express.User, done) => {
    done(null, user.id);
});

passport.deserializeUser(async (id: number, done) => {
    try {
        const user = await deserializeUserById(id);

        done(null, (user as Express.User));
    } catch (error) {
        done(error, null);
    }
});

export { passport }


Enter fullscreen mode Exit fullscreen mode

As you can see the last thing we do is export the passport. Also create a new file called "src/routes/auth/passport/index.ts" and export the passport from there as well:



export { passport } from './passport';


Enter fullscreen mode Exit fullscreen mode

Next we will create the express routes to handle registration and login.


Writing The Express Code

Now that we have our passport code completed we need to actually use it, this is what we will be implemented in this section.

First we need to create two routes, one to register a new user and one to login the user. Create a new file called "src/routes/auth/auth.ts" and add the following imports:



import { Request, Response, NextFunction, Router } from 'express';
import { passport } from './passport';
import { insertUser } from './../../db';


Enter fullscreen mode Exit fullscreen mode

We will then create a Router object and implement the route for the user to use to register a new account:



const router = Router();

router.post('/register', async (req: Request, res: Response) => {
    try {
        if (req.body.password !== req.body.passwordConfirmation) {
            res.status(401).send('Passwords do not match');

            return;
        }

        await insertUser(req.body.email, req.body.password);

        res.status(201).send('User was created');
    } catch (error) {
        res.status(500).send('Error registering user');
    }
});


Enter fullscreen mode Exit fullscreen mode

The above checks if the passwords match up, if ok it will try to insert the user into the PostgreSQL database, if this is successful the user has their data stored in the database and is able to use the next login route to login.

Next we need a route so that the user can login:



router.post('/login', passport.authenticate('local'), (req: Request, res: Response) => {
    console.log('login request');
    res.send('Logged in successfully');
});

export { router };


Enter fullscreen mode Exit fullscreen mode

The above route uses our local passport strategy to authenticate the user, if successful the user is logged in and can access the protected routes that we will create later. We also export the router object.

We also create a helper middleware function that will be used to ensure that the user is logged in and is allowed to access certain routes. Create new directory called "src/routes/auth/helpers" and create a new file called "src/routes/auth/helpers/helpers.ts" and populate it with the following:



import { Request, Response, NextFunction } from 'express';

const ensureAuthenticated = (req: Request, res: Response, next: NextFunction) => {
    if (req.isAuthenticated()) {
        return next();
    }

    res.status(401).send('Unauthorized');
};

export { ensureAuthenticated }


Enter fullscreen mode Exit fullscreen mode

The above makes sure that the user is authenticated and is allowed to access the route that uses this middleware.

Create a new file called "src/routes/auth/helpers/index.ts" and also export the function there:



export { ensureAuthenticated } from './helpers';


Enter fullscreen mode Exit fullscreen mode

Finally create the "src/routes/auth/index.ts" for the auth module:



export { router } from './auth';
export { passport } from './passport';
export { ensureAuthenticated } from './helpers';


Enter fullscreen mode Exit fullscreen mode

In order to test out authentication we will need a route that the user needs to be logged in to see.

Create a new routes directory called "src/routes/protected" and create a new file called "src/routes/protected/protected.ts" and add the following:



import { Request, Response, NextFunction, Router } from 'express';
import { ensureAuthenticated } from './../auth';

const router = Router();

router.get('/protected', ensureAuthenticated, (req: Request, res: Response) => {
    res.send('You are logged in, so you can see this');
});

export { router }


Enter fullscreen mode Exit fullscreen mode

The above route will only be accessible to users that have logged in successfully, next create a new file called "src/routes/protected/index.ts" and export the above:



export { router } from './protected';


Enter fullscreen mode Exit fullscreen mode

Now all we need to do to finish this section is export our express routes from "src/routes/index.ts":



export { router as authRoutes, passport } from './auth';
export { router as protectedRoutes } from './protected';


Enter fullscreen mode Exit fullscreen mode

Here we export both our auth routes and our protected routes.

Now all we need to do is put everything together. πŸ˜‰


Writing The Main Code

The final part, all we have to do now it put everything together. Create a new file called "src/app.ts" and add the following imports:



import express from 'express';
import bodyParser from 'body-parser';
import session from 'express-session';
import { authRoutes, passport, protectedRoutes } from './routes';
import { pool, pgSessionStore, testConnection } from './db';
require('dotenv').config();


Enter fullscreen mode Exit fullscreen mode

The above imports all the stuff we need to code the main application file. Next we decalre a new interface for the user like so:



declare global {
    namespace Express {
        interface User {
            id: number;
            email: string;
            password: string
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Next we will initialize the express application via the following:



const app = express();

app.use(bodyParser.json());


Enter fullscreen mode Exit fullscreen mode

The following will initialize express-session, using the session table we created at the start to store the session information:



app.use(session({
    store: new pgSessionStore({
        pool,
        tableName: 'session'
    }),
    secret: (process.env.SESSION_SECRET as string),
    resave: false,
    saveUninitialized: false,
    cookie: {
        maxAge: 30 * 24 * 60 * 60 * 1000,
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax'
    }
}));


Enter fullscreen mode Exit fullscreen mode

Next we will initialize passport and the routes that we created in the previous section:



app.use(passport.initialize());
app.use(passport.session());

app.use(authRoutes);
app.use(protectedRoutes);


Enter fullscreen mode Exit fullscreen mode

We also want to make sure that we can connect to the PostgreSQL database, this can be done via the following:



testConnection()
    .then(() => {
        console.log('Connection to database success');
    }).catch(() => {
        console.error('Failed to connect to database');
        process.exit(1);
    });


Enter fullscreen mode Exit fullscreen mode

Finally all we need to do is start the express server:



app.listen(3000, () => {
    console.log('Server is running on port 3000');
});


Enter fullscreen mode Exit fullscreen mode

Phew! Now we have coded the application.

Last but not least create a new file called ".env" and populate it with the following making sure to replace the information with your own information:



DATABASE_URL=postgresql://ethan:ethan99@localhost:5432/users
SESSION_SECRET=ethanrules


Enter fullscreen mode Exit fullscreen mode

To build the TypeScript project run the following command:



yarn build


Enter fullscreen mode Exit fullscreen mode

To start the express server run the following command:



node dist/app.js


Enter fullscreen mode Exit fullscreen mode

The server should not be running.


Testing The Application

Next we can actually call the routes to register and login.

You can use something like PostMan, or you can be like me and just use curl.

To register a user run the following command:



curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"email": "user2@example.com", "password": "password", "passwordConfirmation": "password"}'


Enter fullscreen mode Exit fullscreen mode

The above command should register a new user, also if you try running the above command a second time it should fail due to the email being unique.

To login as the above user run the following command:



curl -X POST http://localhost:3000/login \ost:3000/login \
-H "Content-Type: application/json" \
-d '{"email": "user2@example.com", "password": "password"}' \
-c cookies.txt


Enter fullscreen mode Exit fullscreen mode

The above should give you a "Logged in successfully" response, also try using a non existant email/password the request should fail.

Finally if you try to access the protected url with the cookies file created with the above you should get an ok response:



curl -X GET http://localhost:3000/protected \
-b cookies.txt


Enter fullscreen mode Exit fullscreen mode

Try the above route without being authenticated and the request should fail.

Done! Well done for following the tutorial to the end!


Conclusion

Here I have shown how you can implement the passport local strategy into a expressjs nodejs application using Yarn, TypeScript and PostgreSQL.

I hope this helps you as much as I had fun writing it. Also feel free to try and implement logout!

As always you can find the code for this porject on my Github:
https://github.com/ethand91/passport-local-demo

Happy Coding! 😎


Like my work? I post about a variety of topics, if you would like to see more please like and follow me.
Also I love coffee.

β€œBuy Me A Coffee”

If you are looking to learn Algorithm Patterns to ace the coding interview I recommend the [following course](https://algolab.so/p/algorithms-and-data-structure-video-course?affcode=1413380_bzrepgch

Top comments (0)