use dynamic passport strategy nestjs with multiple clientID and clientSecret - node.js

I want to implement passport google and facebook strategy using multiple keys for different apps, like get clientID or something in req params and select google clientID and clientSecret from DB on base of given param i.e users of one application can authenticate using a specific clientID and clientSecret,
want to implement something like this but not sure how to do it in nestjs as iam fairly new to nestjs.
https://medium.com/passportjs/authenticate-using-strategy-instances-49e58d96ec8c
// GoogleStrategy code
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: '', // dynamic key from multiple type of application
clientSecret: '', // dynamic key from multiple type of application
callbackURL: '', // url from user request or hardcoded
scope: ['email', 'profile'], //hardcoded or data from request
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
done(null, profile);
}
}
// Goole Controler
#Controller('google-auth')
export class GoogleAuthController {
constructor(private readonly googleAuthService: GoogleAuthService) {}
#Get('login')
#UseGuards(AuthGuard('google'))
login(#Param('appID') appID: string, #Req() req) {
// Query params to switch between two app type
// e.g app1ID=123132323 or app2ID=2332323
//But what now? The strategy get initiated inside the module
}
#Get('redirect')
#UseGuards(AuthGuard('google'))
redirect(#Req() req) {}
#Get('status')
status() {}
#Get('logout')
logout() {}
}
//GoogleModule
#Module({
imports: [],
controllers: [AppController],
providers: [AppService, GoogleStrategy], //How to use this strategy with multiple
// clientID and clientSecret on base of a
// param
})
export class AppModule {}

Related

How to integrate OpenID connect to Nest JS application

I used this documentation(https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#accounts)for implementing OpenID to Nest JS. In this documentation he mentioned client_id and client secret and redirect URLS. How to get this Information's and Integrating
One option is to create an oidc strategy for passport.
It's a lengthy process, and rather than copying/pasting an entire tutorial, I'll add a link and hope it doesn't break.
https://sdoxsee.github.io/blog/2020/02/05/cats-nest-nestjs-mongo-oidc.html
Here's the strategy implementation, but there are several other components that need to be configured.
// auth/oidc.strategy.ts
import { UnauthorizedException } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { Strategy, Client, UserinfoResponse, TokenSet, Issuer } from 'openid-client';
import { AuthService } from './auth.service';
export const buildOpenIdClient = async () => {
const TrustIssuer = await Issuer.discover(`${process.env.OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER}/.well-known/openid-configuration`);
const client = new TrustIssuer.Client({
client_id: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_ID,
client_secret: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_SECRET,
});
return client;
};
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
client: Client;
constructor(private readonly authService: AuthService, client: Client) {
super({
client: client,
params: {
redirect_uri: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_REDIRECT_URI,
scope: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_SCOPE,
},
passReqToCallback: false,
usePKCE: false,
});
this.client = client;
}
async validate(tokenset: TokenSet): Promise<any> {
const userinfo: UserinfoResponse = await this.client.userinfo(tokenset);
try {
const id_token = tokenset.id_token
const access_token = tokenset.access_token
const refresh_token = tokenset.refresh_token
const user = {
id_token,
access_token,
refresh_token,
userinfo,
}
return user;
} catch (err) {
throw new UnauthorizedException();
}
}
}
You get the client-id and secret from the openid connect provider. You add/register the client there.
Redirect URL is the URL to the openid connect client, to what URL the authorization code should be sent to after a successful authentication. This URL is hardcoded in the provider.

Nestjs passport-facebook Invalid OAuth access token

I'm implementing passport-facebook in my NestJS application. I found many examples and problems with different solutions, and I was trying to implement it in different ways but still no luck.
Here is my strategy:
const FacebookTokenStrategy = require('passport-facebook-token');
#Injectable()
export class FacebookStrategy {
constructor() {
this.init();
}
init() {
use(
new FacebookTokenStrategy(
{
clientID: '...',
clientSecret: '...',
fbGraphVersion: 'v8.0',
profileFields: ['id', 'emails']
},
async (
accessToken: string,
refreshToken: string,
profile: any,
done: any,
) => {
console.log('facebook profile:', profile);
return done(null, null);
},),);}}
And controller handler:
#UseGuards(AuthGuard('facebook-token'))
#Get('facebook')
async getTokenAfterFacebookSignIn(#Req() req) {
console.log(req)
}
Im pretty sure I use correct clientID and clientSecret. On facebook developers tool I generate token like this:
So I use this token to call http://127.0.0.1:3000/auth/facebook?session_token=EAAgSTU1....WpHZAPgZDZD and receive the following error:
oauthError: { statusCode: 400, data: '{"error":{"message":"Invalid OAuth access token.","type":"OAuthException","code":190,"fbtrace_id":"AJqucuXGs8DnoYzT3i5cO7p"}}' }
Where is my mistake? Many thanks!
The problem was wrong query variable name. It must be access_token

Passport Google Oauth2 not prompting select account when only 1 google account logged in

I'm trying to authenticate users in my node + nestjs api and want to prompt the user to select an account.
The prompt does not show up if you have only 1 account logged in and even when you are logged in with 2 accounts and you get prompted, the URL in the redirect still has &prompt=none in the parameters.
I can in fact confirm that it makes no difference that prompt option.
My code simplified below:
import { OAuth2Strategy } from "passport-google-oauth";
import { PassportStrategy } from "#nestjs/passport";
#Injectable()
export class GoogleStrategy extends PassportStrategy(OAuth2Strategy, "google") {
constructor(secretsService: SecretsService) {
super({
clientID: secretsService.get("google", "clientid"),
clientSecret: secretsService.get("google", "clientsecret"),
callbackURL: "https://localhost:3000/auth/google/redirect",
scope: ["email", "profile", "openid"],
passReqToCallback: true,
prompt: "select_account",
});
}
async validate(req: Request, accessToken, refreshToken, profile, done) {
const { name, emails, photos } = profile;
const user = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
accessToken,
};
return done(null, user);
}
}
How can i possibly further debug this to see why/whats happening under the hood?
The actual endpoints:
#Controller("auth")
export class AuthController {
#Get("google")
#UseGuards(AuthGuard("google"))
private googleAuth() {}
#Get("google/redirect")
#UseGuards(AuthGuard("google"))
googleAuthRedirect(#Req() req: Request, #Res() res: Response) {
if (!req.user) {
return res.send("No user from google");
}
return res.send({
message: "User information from google",
user: req.user,
});
}
}
I can't pass an options object with any of the guards or UseGuards decorator.
I've also tried to pass an extra object parameter to the super call but that didn't work either.
Sebastian I've been dealing with this issue as well for about a week. I've finally found what the issue was, and then found that there's a very similar Stack Overflow article that had the same problem:
Auto login while using passport-google-oauth20
The problem comes in when you initialize OAuth2Strategy class with options. It does not pass it's options along to the passport.authenticate(passport, name, options, callback) call since passport.authenticate(...) is only called when you register your middleware handlers for your routes.
You therefore need to pass prompt: 'select_account' when you register passport.authenticate() route middleware
Like so:
router.get(
'/auth/google',
passport.authenticate('google', {
accessType: 'offline',
callbackURL: callbackUrl,
includeGrantedScopes: true,
scope: ['profile', 'email'],
prompt: 'select_account', // <=== Add your prompt setting here
})
);
For anyone use nestjs and facing same issue, here is the solution
class AuthGoogle extends AuthGuard('google') {
constructor() {
super({
prompt: 'select_account'
});
} }
}
// using
#UseGuards(AuthGoogle)
private googleAuth() {}
Sebastian, you can also do this straight from the strategy class itself instead of making a guard specially for invoking the prompt.
Check below.
sample code
Just add this after your constructor call in the strategy class and it will work. You can directly copy the code.

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.
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.
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.

Nestjs + Passport: Prevent user 1 to access information of user 2

How can I prevent user 1 to access information of user 2 using passport in a Nesjs app ?
I already have 2 strategies:
the local strategy which validate a user with email/password. The route protected by this strategy return a jwt token.
the jwt strategy which validate the given jwt token.
Now, I want to restrict access to routes such as users/:id to jwt token which actually have the same userId encrypted.
How to do that ?
EDIT
I was mixing Authentication and Authorization: what I want to achieve is about authorization, once the user has been authenticated.
I had to use Guard:
own.guard.ts
#Injectable()
export class OwnGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return req.user.id === req.params.id;
}
}
Then use it in my route:
#Get(':id')
#UseGuards(OwnGuard)
async get(#Param('id') id: string) {
return await this.usersService.get(id);
}
ORIGINAL ANSWER
What I did was to create a third strategy based on the jwt one:
#Injectable()
export class OwnStrategy extends PassportStrategy(Strategy, 'own') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: SECRET,
passReqToCallback: true
});
}
async validate(req: Request, payload: { sub: string }) {
if (req.params.id !== payload.sub) {
throw new UnauthorizedException();
}
return { userId: payload.sub };
}
}
Note how I pass the custom name 'own' as second parameter of PassportStrategy to differentiate it from the 'jwt' one. Its guard:
#Injectable()
export class OwnAuthGuard extends AuthGuard('own') {}
This works but I wonder if it is the good way of doing it...
What if later I want to able user modification for admin users ?
Should I create a forth strategy which check if role === Role.ADMIN || req.params.id === payload.sub ?
I think I'm missing something. There should be a way to create a strategy which validate only the jwt, another one only the userId, another one only the role, and combine them as I want when applying guards to my routes.
same case. you can use handleRequest method in guard.
here you can access user auth and req, then doing validation for resource appropriate. check out my code
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user, info, context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
const params = request.params;
if (user.id !== +params.id) {
throw new ForbiddenException();
}
return user;
}
}
look more here https://docs.nestjs.com/security/authentication#extending-guards

Resources