# Passport.js

Passport is an 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 Passport, we need to install the Passport.js (opens new window) and the Passport-local.

npm install --save passport
1

# Configure your server

Add this configuration to your server:

import {Configuration, Inject} from "@tsed/di";
import {PlatformApplication} from "@tsed/common";
import "@tsed/passport";
import "@tsed/platform-express";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
import session from "express-session";
import methodOverride from "method-override";

// import your protocol. Ts.ED will discover it automatically
import "./protocols/LoginLocalProtocol";

@Configuration({
  passport: {
    /**
     * Set a custom user info model. By default Ts.ED use UserInfo. Set false to disable Ts.ED json-mapper.
     */
    // userInfoModel: CustomUserInfoModel
    // userProperty: string,
    // pauseStream: string,
    // disableSession: boolean
  }
})
export class Server {
  @Inject()
  app: PlatformApplication;

  $beforeRoutesInit() {
    this.app
      .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
          }
        })
      );
  }
}
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

# UserInfo

By default Ts.ED use a UserInfo model to serialize and deserialize user in session:

import {Format, Property} from "@tsed/schema";

export class UserInfo {
  @Property()
  id: string;

  @Property()
  @Format("email")
  email: string;

  @Property()
  password: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

You can set your own UserInfo model by changing the passport server configuration:

class CustomUserInfoModel {
  @Property()
  id: string;

  @Property()
  token: string;
}

@Configuration({
  passport: {
    userInfoModel: CustomUserInfoModel
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

It's also possible to disable model serialize/deserialize by setting a false value to userInfoModel options.

# 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 is 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 {Req} from "@tsed/common";
    import {Authenticate} from "@tsed/passport";
    import {BodyParams} from "@tsed/platform-params";
    import {Post} from "@tsed/schema";
    import {Controller, ProviderScope, Scope} from "@tsed/di";
    
    @Controller("/auth")
    @Scope(ProviderScope.SINGLETON)
    export class AuthCtrl {
      @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
    14
    15
    16

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

    # Protect a route

    Authorize and Authenticate decorators can be used as a Guard to protect your routes.

    import {QueryParams} from "@tsed/platform-params";
    import {Get} from "@tsed/schema";
    import {Controller, Inject} from "@tsed/di";
    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

    # Basic Auth

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

    import {Req} from "@tsed/common";
    import {Arg, 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, @Arg(0) username: string, @Arg(1) 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

    Then, add the protocol name on the Authorize decorator:

    import {QueryParams} from "@tsed/platform-params";
    import {Get} from "@tsed/schema";
    import {Controller, Inject} from "@tsed/di";
    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

    # Advanced Auth

    # JWT

    JWT auth scenario, for example, is different. The Strategy will produce a payload which contains data and JWT token. This information isn't attached to the request and cannot be retrieved using the default Ts.ED decorator.

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

    For example, the official passport-jwt documentation gives 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
    22
    23

    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

    # Azure Bearer Auth

    Azure bearer uses another scenario which requires to return multiple arguments. The $onVerify method accepts an Array to return multiple values.

    import {Context} from "@tsed/platform-params";
    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(@Arg(0) token: ITokenPayload, @Context() ctx: Context) {
        // Verify is the right place to check given token and return UserInfo
        const {authService} = this;
        const {options = {}} = 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);
            ctx.logger.info({event: "BearerStrategy - token: ", token});
    
            return token;
          }
    
          ctx.logger.info({event: "BearerStrategy - user: ", token});
    
          return [user, token];
        } catch (error) {
          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

    # Discord Auth

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

    Here is 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
    12
    13
    14

    Ts.ED provides 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

    # Facebook Auth

    Facebook passport gives an example to use scope on routes (permissions). We'll see how we can configure a route with a mandatory scope.

    Here is the corresponding Facebook protocol:

    import {Req} from "@tsed/common";
    import {Inject} from "@tsed/di";
    import {Args, OnInstall, OnVerify, Protocol} from "@tsed/passport";
    import {Strategy, StrategyOptions} from "passport-facebook";
    import {AuthService} from "../services/auth/AuthService";
    
    @Protocol<StrategyOptions>({
      name: "facebook",
      useStrategy: Strategy,
      settings: {
        clientID: "FACEBOOK_APP_ID",
        clientSecret: "FACEBOOK_APP_SECRET",
        callbackURL: "http://www.example.com/auth/facebook/callback",
        profileFields: ["id", "emails", "name"]
      }
    })
    export class FacebookProtocol implements OnVerify, OnInstall {
      @Inject()
      private authService: AuthService;
    
      async $onVerify(@Req() req: Req, @Args() [accessToken, refreshToken, profile]: any) {
        profile.refreshToken = refreshToken;
    
        const user = await this.authService.findOne({facebookId: profile.id});
    
        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
    27
    28

    Note

    In order to use Facebook authentication, you must first create an app at Facebook Developers. When created, an app is assigned an App ID and an App Secret. Your application must also implement a redirect URL, to which Facebook will redirect users after they have approved access for your application.

    The verify callback for Facebook authentication accepts accessToken, refreshToken, and profile arguments. profile will contain user profile information provided by Facebook; refer to User Profile (opens new window) for additional information.

    WARNING

    For security reasons, the redirection URL must reside on the same host that is registered with Facebook.

    Then we have to implement routes as following:

    import {Req} from "@tsed/common";
    import {Get} from "@tsed/schema";
    import {Controller} from "@tsed/di";
    import {Authenticate} from "@tsed/passport";
    
    @Controller("/auth")
    export class AuthCtrl {
      @Get("/:provider")
      @Authenticate("facebook", {scope: ["email"]})
      authenticated(@Req("user") user: Req) {
        // Facade
        return user;
      }
    
      @Get("/:provider/callback")
      @Authenticate("facebook")
      callback(@Req("user") user: Req) {
        // Facade
        return user;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    Note

    Authenticate decorator accepts a second option to configure the scope. It is equivalent to passport.authenticate('facebook', {scope: 'read_stream' })

    # Roles

    Roles access management isn't a part of Passport.js and Ts.ED doesn't provide a way to handle this because it is specific for each application.

    This section will give basic examples to implement your own roles strategy access.

    To begin we have to implement a middleware which will be responsible to check the user role:

    import {Middleware} from "@tsed/platform-middlewares";
    import {Context} from "@tsed/platform-params";
    import {Unauthorized} from "@tsed/exceptions";
    
    @Middleware()
    export class AcceptRolesMiddleware {
      use(@Context() ctx: Context) {
        const request = ctx.getReq();
    
        if (request.user && request.isAuthenticated()) {
          const roles = ctx.endpoint.get(AcceptRolesMiddleware);
    
          if (!roles.includes(request.user.role)) {
            throw new Unauthorized("Insufficient role");
          }
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    Then, we have to create a decorator AcceptRoles. This decorator will store the given roles and register the AcceptRolesMiddleware created before.

    import {UseBefore} from "@tsed/platform-middlewares";
    import {useDecorators, StoreSet} from "@tsed/core";
    import {AcceptRolesMiddleware} from "./AcceptRolesMiddleware";
    
    export function AcceptRoles(...roles: string[]) {
      return useDecorators(UseBefore(AcceptRolesMiddleware), StoreSet(AcceptRolesMiddleware, roles));
    }
    
    1
    2
    3
    4
    5
    6
    7

    Finally, we can use this decorator on an Endpoint like this:

    import {Post} from "@tsed/schema";
    import {Controller} from "@tsed/di";
    import {Authorize} from "@tsed/passport";
    import {AcceptRoles} from "../decorators/acceptRoles";
    
    @Controller("/calendars")
    export class CalendarController {
      @Post("/")
      @Authorize("local")
      @AcceptRoles("admin")
      async post() {}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # Catch Passport Exception 6.18.0+

    import {Catch, ExceptionFilterMethods, PlatformContext} from "@tsed/common";
    import {PassportException} from "@tsed/passport";
    
    @Catch(PassportException)
    export class PassportExceptionFilter implements ExceptionFilterMethods {
      async catch(exception: PassportException, ctx: PlatformContext) {
        const {response} = ctx;
    
        console.log(exception.name);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # Decorators

    Loading in progress...

    # Author

      # Maintainers Help wanted

        Last Updated: 3/15/2024, 12:53:49 PM

        Other topics