'NestJS | Using OAuth along with Session

I'm using NestJS as a Service then I've got this problem. I don't usually deal with sessions and tokens.

I've been able to implement the session via passport-local, but I'm having a problem regarding Facebook and Google OAuth login it's not saving the session in the database, while the local guard does.

Below are the codes for the strategies and guards.

File: local.strategy.ts


import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth.service';
import { VerifyUserDto } from 'src/user/dto/VerifyUser.dto';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      usernameField: 'UserID',
      passwordField: 'UserPass',
    });
  }

  async validate(UserID: string, UserPass: string): Promise<any> {
    const ToBeVerified = new VerifyUserDto();
    ToBeVerified.UserID = UserID;
    ToBeVerified.UserPass = UserPass;
    return await this.authService.ValidateUser(ToBeVerified);
  }
}

File: google.strategy.ts

import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_SECRET,
      callbackURL: '/api/v1/google/redirect',
      scope: ['email', 'profile'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: VerifyCallback,
  ): Promise<any> {
    const data = {
      accessToken,
      profile,
      // refreshToken,
    };
    done(null, data);
  }
}

File: facebook.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-facebook';

@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
  constructor() {
    super({
      clientID: process.env.FACEBOOK_CLIENT_ID,
      clientSecret: process.env.FACEBOOK_SECRET,
      callbackURL: '/api/v1/facebook/redirect',
      scope: ['email', 'profile'],
      // profileFields: ['emails', 'name'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: Profile,
    done: (err: any, user: any, info?: any) => void,
  ): Promise<any> {
    const data = {
      accessToken,
      profile,
    };
    done(null, data);
  }
}

File: Local Guard

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

@Injectable()
export class LocalGuard extends AuthGuard('local') {
  async canActivate(context: ExecutionContext) {
    const result = (await super.canActivate(context)) as boolean;
    const request = context.switchToHttp().getRequest();
    await super.logIn(request);
    return result;
  }
}

export class Authenticated implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    return request.isAuthenticated();
  }
}

File: Google Guard

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

@Injectable()
export class GoogleGuard extends AuthGuard('google') {
  async canActivate(context: ExecutionContext) {
    const result = (await super.canActivate(context)) as boolean;
    const request = context.switchToHttp().getRequest();
    await super.logIn(request);
    return result;
  }
}

export class Authenticated implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    return request.isAuthenticated();
  }
}

File: Facebook Guard

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

@Injectable()
export class FacebookGuard extends AuthGuard('facebook') {
  async canActivate(context: ExecutionContext) {
    const result = (await super.canActivate(context)) as boolean;
    const request = context.switchToHttp().getRequest();
    await super.logIn(request);
    return result;
  }
}

export class Authenticated implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    return request.isAuthenticated();
  }
}


Solution 1:[1]

I will share with You my code for google autorization

import { Injectable }               from '@nestjs/common';
import { ModuleRef }                from '@nestjs/core';
import { PassportStrategy }         from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { AuthService }              from '../services/auth.service';


export interface StrategyProfile<STRATEGY extends string> {
  id: string;
  displayName: string;
  name: {
    familyName: string; givenName: string;
  };
  emails: { value: string; verified: boolean; }[];
  photos: { value: string; }[];
  provider: STRATEGY;
}

export interface UserExternalAuthorization {
  providerId: string;
  providerName: string;
  
  eMail: string;
  name: string;
  avatar: string;
}

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(private authService: AuthService, private moduleRef: ModuleRef) {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID || '',
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
      callbackURL: 'http://localhost:3000/auth/redirect/google',
      scope: [ 'email', 'profile', ],
      // scope: [ 'email', 'profile', 'https://www.googleapis.com/auth/user.birthday.read' ], // not reading the date
    });
  }
  
  async validate(
    accessToken: string,
    refreshToken: string,
    profile: StrategyProfile<'google'>,
    done: VerifyCallback,
  ): Promise<any> {
    const email = profile.emails.find((x) => x.verified);
    if (!email) {
      throw new Error('No verified email returned from Google Authorization!');
    }
    
    const user: UserExternalAuthorization = {
      providerId: profile.id,
      providerName: profile.provider,
      
      eMail: email.value,
      name: profile.displayName,
      avatar: profile?.photos[0]?.value,
    };
    
    const u = await this.authService.externalUser(user);
    
    done(null, u);
  }
}

Function return await this.authService.ValidateUser(ToBeVerified); from local strategy, check against password (if its correct) and then returns user object im using for jwt (userId, loginType(google, local etc), userType(admin or not) and some other simple data - not many).

Function await this.authService.externalUser(user); from google-strategy returns same object, but also

  • creates user in the database (by email) if there was no such user before
  • do not check password (just is user was already there and was removed/banned)

So in every part of the system tokens im using have the same structure and i dont care from where user came).

As for Guard, im using one for each user (as i dont want to distinguish them on this level yet). AuthService is service for logging users and for changing password (even if you are not from local, your account is created and you would be able to log in using password unless you check different option in settings)

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard }                    from '@nestjs/passport';
import { Observable }                   from 'rxjs';
import { AuthService }                  from '../services/auth.service';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  
  constructor(
    protected readonly dbService: AuthService,
  ) {super();}
  
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const can = super.canActivate(context);
    
    if (can instanceof Promise) {
      return new Promise(async (resolve, reject) => {
        can.then((c) => {
          // c !== true -> log notLogged
          resolve(c);
        }).catch((e) => {
          // logged denied, e.message
          reject(e);
        });
      });
    } else {
      return can;
    }
  }
}

Also i made decorator for using in controllers

import { applyDecorators, UseGuards }             from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { JwtAuthGuard }                           from '../guards/jwt-auth.guard';

export function IsLogged() {
  return applyDecorators(
    UseGuards(JwtAuthGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({description: 'Unauthorized'}),
  );
}

So in controllers you can have something like that

  • everything in controller locked for logged users
@IsLogged()
@Controller()
export class name {}
  • just end point locked only for logged users
@Controller()
export class name {

  @IsLogged()
  @Get('/')
  async fName() {}
}

I wish this helps and leverage some overburden of using many AuthGuard types

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Seti