Passport.js

Passport is authentication middleware for Node.js.

Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.

Installation

Before using the Passport, we need to install the Passport.js and the Passport-local.

npm install --save passport
1

Override AuthenticatedMiddleware

The annotation @Authenticated() use the AuthenticatedMiddleware to check the authentication strategy.

So, create a new file in your middlewares directory and past this code:

import {OverrideMiddleware, AuthenticatedMiddleware} from "@tsed/common";
import {Forbidden} from "ts-httpexceptions";

@OverrideMiddleware(AuthenticatedMiddleware)
export class MyAuthenticatedMiddleware implements IMiddleware {
    public use(@EndpointInfo() endpoint: EndpointMetadata,
               @Request() request: Express.Request,
               @Next() next: Express.NextFunction) { // next is optional here
        
        // options given to the @Authenticated decorator
        const options = endpoint.get(AuthenticatedMiddleware) || {};
        // options => {role: 'admin'}
        
        if (!request.isAuthenticated()) { // passport.js
          throw new Forbidden("Forbidden")  
        }
        
        next();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Local strategy

Now, we need to expose some routes to enable the login, the signup and the logout. To do that, we'll use the passport-local strategy and we create a passport service and a passport controller.

The PassportLocalService

In the service directory, we'll create the PassportLocalServices.ts and write this code:

import * as Passport from "passport";
import {Strategy} from "passport-local";
import {Service, BeforeRoutesInit, AfterRoutesInit} from "@tsed/common";
import {UserService} from "./UserService"; // other service that manage the users account

@Service()
export class PassportLocalService implements BeforeRoutesInit, AfterRoutesInit {

    constructor(private serverSettings: ServerSettingsService,
                private userService: UserService,
                @Inject(ExpressApplication) private  expressApplication: ExpressApplication) {

    }
    
    $beforeRoutesInit() {
        const options: any = this.serverSettings.get("passport") || {} as any;
        const {userProperty, pauseStream} = options; // options stored with ServerSettings

        this.expressApplication.use(Passport.initialize({userProperty}));
        this.expressApplication.use(Passport.session({pauseStream}));
    }

    $afterRoutesInit() {
        this.initializeSignup();
        this.initializeLogin();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

We use the hook service licecycle to autoloading some actions when the server start. See the service lifecycle for more informations.

Passport controller

We'll need to prepare some routes. To work it, the Passport need 3 routes:

  • /login, email and password will be sent in the body request,
  • /signup, email, password and optional information will be sent in the body request,
  • /logout.

So create the PassportCtrl in the controllers directory and put this code:

"use strict";

import * as Express from "express";
import {BodyParams, Controller, Get, Post, Req, Required, Res} from "@tsed/common";

@Controller("/passport")
export class PassportCtrl {

    @Post("/login")
    async login(@Required() @BodyParams("email") email: string,
                @Required() @BodyParams("password") password: string,
                @Req() request: Express.Request,
                @Res() response: Express.Response) {

    }

    @Post("/signup")
    async signup(@Required() @BodyParams("email") email: string,
                 @Required() @BodyParams("password") password: string,
                 @Required() @BodyParams("firstName") firstName: string,
                 @Required() @BodyParams("lastName") lastName: string,
                 @Req() request: Express.Request,
                 @Res() response: Express.Response) {

    }

    @Get("/logout")
    public logout(@Req() request: Express.Request): string {
        
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

Signup

We will use Passport to register the signup action with the LocalStrategy. Passport will call back the handler when the the PassportCtrl call the Passport.authenticate('signup') method.

In the PassportCtrl, we need to implement the Passport.authenticate('signup') like this:

import * as Express from "express";
import * as Passport from "passport";
import {BodyParams, Controller, Get, Post, Req, Required, Res} from "@tsed/common";
import {IUser} from "../../interfaces/User";

@Controller("/passport")
export class PassportCtrl {

    @Post("/signup")
    async signup(@Required() @BodyParams("email") email: string,
                 @Required() @BodyParams("password") password: string,
                 @Required() @BodyParams("firstName") firstName: string,
                 @Required() @BodyParams("lastName") lastName: string,
                 @Req() request: Express.Request,
                 @Res() response: Express.Response) {
        return new Promise((resolve, reject) => {

            Passport.authenticate("signup", (err, user: IUser) => {

                if (err) {
                    return reject(err);
                }

                if (!user) {
                    return reject(!!err);
                }

                request.logIn(user, (err) => {

                    if (err) {
                        return reject(err);
                    }
                    resolve(user);
                });
            })(request, response, () => {

            });
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

Now, when a Request is sent to the signup route, Passport will emit a signup event.

In your PassportLocalService, we able to implement the Passport Local strategy on the signup event.

import * as Passport from "passport";
import {Strategy} from "passport-local";
import {Service, BeforeRoutesInit, AfterRoutesInit} from "@tsed/common";
import {BadRequest} from "ts-httpexceptions";

@Service()
export class PassportLocalService implements BeforeRoutesInit, AfterRoutesInit {

    //...
    
    public initializeSignup() {

        Passport
            .use("signup", new Strategy({
                    // by default, local strategy uses username and password, we will override with email
                    usernameField: "email",
                    passwordField: "password",
                    passReqToCallback: true // allows us to pass back the entire request to the callback
                },
                (req, email, password, done) => {
                    const {firstName, lastName} = req.body;
                    // asynchronous
                    // User.findOne wont fire unless data is sent back
                    process.nextTick(() => {
                        this.signup({
                            firstName,
                            lastName,
                            email,
                            password
                        })
                            .then((user) => done(null, user))
                            .catch((err) => done(err));
                    });
                }));

    }

    /**
     *
     * @param user
     * @returns {Promise<any>}
     */
    async signup(user: IUser) {

        const exists = await this.usersService.findByEmail(user.email);

        if (exists) { //User exists
            throw new BadRequest("Email is already registered");
        }
        
        // Promise

        // Create new User
        return await this.usersService.create(<any>{
            email: user.email,
            password: user.password,
            firstName: user.firstName,
            lastName: user.lastName
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

We'll not implement the userService. We'll assume that it'll do what is expected.

Login

Implement login is the same principle that the signup. In the controller we need to use the Passport.authenticate("login") method to emit the login event to each Passport Strategy.

import * as Express from "express";
import * as Passport from "passport";
import {BodyParams, Controller, Get, Post, Req, Required, Res} from "@tsed/common";
import {IUser} from "../../interfaces/User";

@Controller("/passport")
export class PassportCtrl {

    @Post("/login")
    async login(@Required() @BodyParams("email") email: string,
                @Required() @BodyParams("password") password: string,
                @Req() request: Express.Request,
                @Res() response: Express.Response) {


        return new Promise<IUser>((resolve, reject) => {
            Passport
                .authenticate("login", (err, user: IUser) => {
                    if (err) {
                        reject(err);
                    }

                    request.logIn(user, (err) => {

                        if (err) {
                            reject(err);
                        } else {
                            resolve(user);
                        }
                    });

                })(request, response, () => {

                });
        });

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

In your service we can do that:


import * as Passport from "passport";
import {Strategy} from "passport-local";
import {Service, BeforeRoutesInit, AfterRoutesInit} from "@tsed/common";
import {BadRequest, NotFound} from "ts-httpexceptions";

@Service()
export class PassportLocalService implements BeforeRoutesInit, AfterRoutesInit {

    //...
    public initializeLogin() {
        Passport.use("login", new Strategy({
            // by default, local strategy uses username and password, we will override with email
            usernameField: "email",
            passwordField: "password",
            passReqToCallback: true // allows us to pass back the entire request to the callback
        }, (req, email, password, done) => {
            this.login(email, password)
                .then((user) => done(null, user))
                .catch((err) => done(err));
        }));
    }
    
    async login(email: string, password: string): Promise<IUser> {
        const user = await this.usersService.findByCredential(email, password);
        if (user) {
            return user;
        }

        throw new NotFound("User not found");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

Logout

Logout is very short, just place this code in the PassportCtrl and it's done:

import * as Express from "express";
import {BodyParams, Controller, Get, Post, Req, Required, Res} from "@tsed/common";
import {IUser} from "../../interfaces/User";

@Controller("/passport")
export class PassportCtrl {
    @Get("/logout")
    public logout(@Req() request: Express.Request): string {
        request.logout();
        return "Disconnected";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

TIP

You can find a working example on Passport.js here.