# 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

# Configure your server

Add this configuration to your server:

import {ServerLoader, ServerSettings} from "@tsed/common";
import "@tsed/passport";
import * as methodOverride from "method-override";
import * as bodyParser from "body-parser";
import * as cookieParser from "cookie-parser";
import * as session from "express-session";

const rootDir = __dirname;

@ServerSettings({
  componentsScan: [
    `${rootDir}/protocols/*{.ts,.js}` // scan protocols directory
  ],
  passport: {}
})
export class Server extends ServerLoader {
  $beforeRoutesInit() {
    this
      .use(cookieParser())
      .use(methodOverride())
      .use(bodyParser.json())
      .use(bodyParser.urlencoded({
        extended: true
      }))
      // @ts-ignore
      .use(session({
        secret: "mysecretkey",
        resave: true,
        saveUninitialized: true,
        // maxAge: 36000,
        cookie: {
          path: "/",
          httpOnly: true,
          secure: false,
          maxAge: null
        }
      }));
  }
}
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

# Create a new Protocol

A Protocol is a special Ts.ED service which is used to declare a Passport Strategy and handle Passport lifecycle.

Here an example with the PassportLocal:

    TIP

    For signup and basic flow you can checkout one of our examples:

    # Create the Passport controller

    Create a new Passport controller as following:

    import {BodyParams, Controller, Post, ProviderScope, Req, Scope} from "@tsed/common";
    import {Authenticate} from "@tsed/passport";
    
    @Controller("/")
    @Scope(ProviderScope.SINGLETON)
    export class PassportCtrl {
      @Post("/login")
      @Authenticate("login")
      login(@Req() req: Req, @BodyParams("email") email: string, @BodyParams("password") password: string) {
        // FACADE
        return req.user;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    This controller will provide required all endpoints which will be used by the different protocols.

    # Protect a route

    and decorator can be used as a Guard to protect your routes.

    import {Controller, Get, QueryParams, Inject} from "@tsed/common";
    import {Authorize} from "@tsed/passport";
    import {Calendar} from "../models/Calendar";
    import {CalendarsService} from "../service/CalendarsService";
    
    @Controller("/calendars")
    export class CalendarController {
      @Inject()
      private calendarsService: CalendarsService;
    
      @Get("/")
      @Authorize()
      async getAll(
        @QueryParams("id") id: string,
        @QueryParams("name") name: string,
        @QueryParams("owner") owner: string
      ): Promise<Calendar[]> {
        return this.calendarsService.findAll({_id: id, name, owner});
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    # Basic Auth

    It also possible to use the Basic Auth. To doing that, you have to create a Protocol based on passport-http strategy.

    import {BodyParams, Req} from "@tsed/common";
    import {OnInstall, OnVerify, Protocol} from "@tsed/passport";
    import {Strategy} from "passport";
    import {BasicStrategy} from "passport-http";
    import {UsersService} from "../services/users/UsersService";
    import {checkEmail} from "../utils/checkEmail";
    
    @Protocol({
      name: "basic",
      useStrategy: BasicStrategy,
      settings: {}
    })
    export class BasicProtocol implements OnVerify, OnInstall {
      constructor(private usersService: UsersService) {
      }
    
      async $onVerify(@Req() request: Req, @BodyParams("username") username: string, @BodyParams("password") password: string) {
        checkEmail(username);
    
        const user = await this.usersService.findOne({email: username});
    
        if (!user) {
          return false;
        }
    
        if (!user.verifyPassword(password)) {
          return false;
        }
    
        return user;
      }
    
      $onInstall(strategy: Strategy): void {
        // intercept the strategy instance to adding extra configuration
      }
    }
    
    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

    Then, add the protocol name on decorator:

    import {Controller, Get, Inject, QueryParams} from "@tsed/common";
    import {Authorize} from "@tsed/passport";
    import {Calendar} from "../models/Calendar";
    import {CalendarsService} from "../service/CalendarsService";
    
    @Controller("/calendars")
    export class CalendarController {
      @Inject()
      private calendarsService: CalendarsService;
    
      @Get("/")
      @Authorize("basic")
      async getAll(
        @QueryParams("id") id: string,
        @QueryParams("name") name: string,
        @QueryParams("owner") owner: string
      ): Promise<Calendar[]> {
        return this.calendarsService.findAll({_id: id, name, owner});
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    # Advanced Auth

    # JWT

    JWT auth scenario for example is different. The Strategy will produce a payload which contain data and jwt token. This information aren't attached no the request and cannot be retrieve by using the default Ts.ED decorator.

    To solve it, the @tsed/passport has two decorators and to get argument given to the original verify function by the Strategy.

    For example, the official passport-jwt documentation give this javascript code to configure the strategy:

    const {JwtStrategy, ExtractJwt} = require("passport-jwt");
    const opts = {};
    
    opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
    opts.secretOrKey = "secret";
    opts.issuer = "accounts.examplesoft.com";
    opts.audience = "yoursite.net";
    
    passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
      authService.findOne({id: jwt_payload.sub}, function(err, user) {
        if (err) {
          return done(err, false);
        }
        if (user) {
          return done(null, user);
        } else {
          return done(null, false);
          // or you could create a new account
        }
      });
    }));
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    The example code can be written with Ts.ED as following:

    import {Req} from "@tsed/common";
    import {Arg, OnVerify, Protocol} from "@tsed/passport";
    import {ExtractJwt, Strategy, StrategyOptions} from "passport-jwt";
    import {AuthService} from "../services/auth/AuthService";
    
    @Protocol<StrategyOptions>({
      name: "jwt",
      useStrategy: Strategy,
      settings: {
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
        secretOrKey: "secret",
        issuer: "accounts.examplesoft.com",
        audience: "yoursite.net"
      }
    })
    export class JwtProtocol implements OnVerify {
      constructor(private authService: AuthService) {
      }
    
      async $onVerify(@Req() req: Req, @Arg(0) jwtPayload: any) {
        const user = await this.authService.findOne({id: jwtPayload.sub});
    
        return user ? user : false;
      }
    }
    
    
    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

    # Azure Bearer Auth

    Azure bearer, use another scenario which require to return multiple argument. The $onVerify method accept an Array to return multiple value.

    import {Req} from "@tsed/common";
    import {Arg, OnVerify, PassportMiddleware, Protocol} from "@tsed/passport";
    import {BearerStrategy, ITokenPayload} from "passport-azure-ad";
    import {AuthService} from "../services/auth/AuthService";
    
    @Protocol({
      name: "azure-bearer",
      useStrategy: BearerStrategy
    })
    export class AzureBearerProtocol implements OnVerify {
      constructor(private authService: AuthService) {
      }
    
      $onVerify(@Req() req: Req, @Arg(0) token: ITokenPayload) {
        // Verify is the right place to check given token and return UserInfo
        const {authService} = this;
        const {options = {}} = req.ctx.endpoint.get(PassportMiddleware) || {}; // retrieve options configured for the endpoint
        // check precondition and authenticate user by their token and given options
        try {
          const user = authService.verify(token, options);
    
          if (!user) {
            authService.add(token);
            req.ctx.logger.info({event: "BearerStrategy - token: ", token});
    
            return token;
          }
    
          req.ctx.logger.info({event: "BearerStrategy - user: ", token});
    
          return [user, token];
        } catch (error) {
          req.ctx.logger.error({event: "BearerStrategy", token, error});
          throw error;
        }
      }
    }
    
    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

    # Discord Auth

    Discord passport give an example to refresh the token. To doing that you have to create a new Strategy and register with the refresh function from passport-oauth2-refresh module.

    Here the JavaScript code:

    const {Strategy} = require("passport-discord");
    
    passport
      .use(new Strategy({
          clientID: "id",
          clientSecret: "secret",
          callbackURL: "callbackURL"
        },
        (accessToken, refreshToken, profile, cb) => {
          authService.findOrCreate({discordId: profile.id}, cb);
        }));
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    Ts.ED provide a way to handle the strategy built by the @tsed/passport by using the $onInstall hook.

    import {Req} from "@tsed/common";
    import {Args, OnInstall, OnVerify, Protocol} from "@tsed/passport";
    import {Strategy, StrategyOptions} from "passport-discord";
    import * as refresh from "passport-oauth2-refresh";
    import {AuthService} from "../services/auth/AuthService";
    
    @Protocol<StrategyOptions>({
      name: "discord",
      useStrategy: Strategy,
      settings: {
        clientID: "id",
        clientSecret: "secret",
        callbackURL: "callbackURL"
      }
    })
    export class DiscordProtocol implements OnVerify, OnInstall {
      constructor(private authService: AuthService) {
      }
    
      async $onVerify(@Req() req: Req, @Args() [accessToken, refreshToken, profile]: any) {
        profile.refreshToken = refreshToken;
    
        const user = await this.authService.findOne({discordId: profile.id});
    
        return user ? user : false;
      }
    
      async $onInstall(strategy: Strategy) {
        refresh.use(strategy);
      }
    }
    
    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

    # Decorators