facebook-passport with NestJS
Asked Answered
A

2

11

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.

Airframe answered 16/5, 2020 at 1:46 Comment(0)
B
5

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.

Bureaucrat answered 16/5, 2020 at 15:53 Comment(5)
thanks so much Jay, I am not sure how I overlooked this. it is exactly what I need.Airframe
Do you have any idea what type of object err is? it seems to be a string which is difficult to parseAirframe
Nope. One of the problems is passport is not a typed package, so who knows what error it is returning.Bureaucrat
This answer also saved me a lot of time.. Maybe they should write it better at the documentation, i also like @Airframe was like 'How did i overlook this!'Overrule
How would you propose it written better? It's already linkable, which means it has a header on the page. And every passport strategy is usable with Nest by creating a Strategy class and using the built in AuthGuard().Bureaucrat
G
2

In my case, I used to use the passport-facebook-token with older version of nest. To upgrade, the adjustment of the strategy was needed. I am also not interested in the callback url.

This is a working version with passport-facebook-token that uses nest conventions and benefits from dependency injection:

import { Injectable } from '@nestjs/common'

import { PassportStrategy } from '@nestjs/passport'
import * as FacebookTokenStrategy from 'passport-facebook-token'

import { UserService } from '../user/user.service'
import { FacebookUser } from './types'

@Injectable()
export class FacebookStrategy extends PassportStrategy(FacebookTokenStrategy, 'facebook-token') {
  constructor(private userService: UserService) {
    super({
      clientID: process.env.FB_CLIENT_ID,
      clientSecret: process.env.FB_CLIENT_SECRET,
    })
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: FacebookTokenStrategy.Profile,
    done: (err: any, user: any, info?: any) => void,
  ): Promise<any> {
    const userToInsert: FacebookUser = {
      ...
    }

    try {
      const user = await this.userService.findOrCreateWithFacebook(userToInsert)

      return done(null, user.id) // whatever should get to your controller
    } catch (e) {
      return done('error', null)
    }
  }
}

This creates the facebook-token that can be used in the controller.

Grethel answered 3/1, 2022 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.