Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

facebook-passport with NestJS

I have looked into both passport-facebook and passport-facebook-token integration with NestJS. The problem is that NestJS abstracts passport implementation with its own utilities such as AuthGuard.

Because of this, ExpressJS style implementation that's documented will not work with NestJS. This for instance is not compliant with the @nestjs/passport package:

var FacebookTokenStrategy = require('passport-facebook-token');

passport.use(new FacebookTokenStrategy({
    clientID: FACEBOOK_APP_ID,
    clientSecret: FACEBOOK_APP_SECRET
  }, function(accessToken, refreshToken, profile, done) {
    User.findOrCreate({facebookId: profile.id}, function (error, user) {
      return done(error, user);
    });
  }
));

This blog post shows one strategy for implementing passport-facebook-token using an unfamiliar interface that isn't compliant with AuthGuard.

@Injectable()
export class FacebookStrategy {
  constructor(
    private readonly userService: UserService,
  ) {
    this.init();
  }
  init() {
    use(
      new FacebookTokenStrategy(
        {
          clientID: <YOUR_APP_CLIENT_ID>,
          clientSecret: <YOUR_APP_CLIENT_SECRET>,
          fbGraphVersion: 'v3.0',
        },
        async (
          accessToken: string,
          refreshToken: string,
          profile: any,
          done: any,
        ) => {
          const user = await this.userService.findOrCreate(
            profile,
          );
          return done(null, user);
        },
      ),
    );
  }
}

The problem here is that this seems to be completely unconventional to how NestJS expects you to handle a passport strategy. It is hacked together. It could break in future NestJS updates as well. There's also no exception handling here; I have no way to capture exceptions such as InternalOAuthError which gets thrown by passport-facebook-token because of the callback nature that's being utilized.

Is there a clean way to implement either one of passport-facebook or passport-facebook-token so that it'll use @nestjs/passport's validate() method? From the documentation: For each strategy, Passport will call the verify function (implemented with the validate() method in @nestjs/passport). There should be a way to pass a clientId, clientSecret in the constructor and then put the rest of the logic into the validate() method.

I would imagine the final result to look something similar to the following (this does not work):

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import FacebookTokenStrategy from "passport-facebook-token";


@Injectable()
export class FacebookStrategy extends PassportStrategy(FacebookTokenStrategy, 'facebook')
{

    constructor()
    {
        super({
            clientID    : 'anid',     // <- Replace this with your client id
            clientSecret: 'secret', // <- Replace this with your client secret
        })
    }


    async validate(request: any, accessToken: string, refreshToken: string, profile: any, done: Function)
    {
        try
        {
            console.log(`hey we got a profile: `, profile);

            const jwt: string = 'placeholderJWT'
            const user = 
            {
                jwt
            }

            done(null, user);
        }
        catch(err)
        {
            console.log(`got an error: `, err)
            done(err, false);
        }
    }

}

In my particular case, I am not interested in callbackURL. I am just validating an access token that the client has forwarded to the server. I just put the above to be explicit.

Also if you are curious, the code above produces an InternalOAuthError but I have no way of capturing the exception in the strategy to see what the real problem is because it isn't implemented correctly. I know that in this particular case the access_token I am passing is invalid, if I pass a valid one, the code works. With a proper implementation though I would be able to capture the exception, inspect the error, and be able to bubble up a proper exception to the user, in this case an HTTP 401.

InternalOAuthError: Failed to fetch user profile

It seems clear that the exception is being thrown outside of the validate() method, and that's why our try/catch block is not capturing the InternalOAuthError. Handling this exception is critical for normal user experience and I am not sure what the NestJS way of handling it is in this implementation or how error handling should be done.

like image 642
randombits Avatar asked May 16 '20 01:05

randombits


People also ask

What is passport in Nestjs?

Passport is the most popular node.js authentication library, well-known by the community and successfully used in many production applications. It's straightforward to integrate this library with a Nest application using the @nestjs/passport module.


1 Answers

You're on the right track with the Strategy using extends PassportStrategy() class setup you have going. In order to catch the error from passport, you can extend the AuthGuard('facebook') and add some custom logic to handleRequest(). You can read more about it here, or take a look at this snippet from the docs:

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // Add your custom authentication logic here
    // for example, call super.logIn(request) to establish a session.
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

Yes, this is using JWT instead of Facebook, but the underlying logic and handler are the same so it should still work for you.

like image 67
Jay McDoniel Avatar answered Oct 06 '22 19:10

Jay McDoniel