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.