R.Muneeb
R.Muneeb

Reputation: 41

How to get id_token from passport-google-oauth20 package - NestJs

I am new to NestJs and trying to implement Google Sign in using passport-google-oauth20 package. I have followed that blog to implement google sign in. Through this package I am able to successfully signed-in and able to get access_token but I need id_token instead of access_token. I dug into the passport-google-oauth20 Strategy class and there I can see different overloaded constructors where one overloaded constructor contains params argument of type GoogleCallbackParameters which contains optional id_token field. But don't know how to make that constructor called. Tried different ways but with no success. :(

Below is my code,

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Request } from "express";
import { Profile } from "passport";
import {
  GoogleCallbackParameters,
  Strategy,
  VerifyCallback,
} from "passport-google-oauth20";
import { googleStrategy } from "src/utils/constants";

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
  constructor() {
    super({
      clientID:
        process.env.BACKEND_ENV === "dev"
          ? googleStrategy.GOOGLE_CLIENT_ID
          : process.env.GOOGLE_CLIENT_ID,
      clientSecret:
        process.env.BACKEND_ENV === "dev"
          ? googleStrategy.GOOGLE_CLIENT_SECRET
          : process.env.GOOGLE_CLIENT_SECRET,
      callbackURL:
        process.env.BACKEND_ENV === "dev"
          ? googleStrategy.GOOGLE_CALLBACK_URL
          : process.env.GOOGLE_CALLBACK_URL,
      scope: ["email", "profile", "openid"],
      passReqToCallback: true,
    });
  }

  async validate(
    req: Request,
    accessToken: string,
    refreshToken: string,
    params: GoogleCallbackParameters,
    profile: Profile,
    done: VerifyCallback,
  ): Promise<any> {
    const { name, emails, photos } = profile;
    const user = {
      email: emails[0].value,
      firstName: name.givenName,
      lastName: name.familyName,
      picture: photos[0].value,
      accessToken,
      refreshToken,
    };

    done(null, user);
  }
}

As you can see for getting Request, I have mentoned passReqToCallback: true option and in the validate method I am getting the Request object but don't know how to make params of type GoogleCallbackParameters get filled with the required object.

Thanks.

Upvotes: 3

Views: 1048

Answers (3)

jinnux
jinnux

Reputation: 1

After a while digging into this problem, I think I can give anyone interested some information.

TL;DR

Using export class GoogleStrategy extends PassportStrategy(Strategy, 'google', 5) allows using validate normally without callback function in super. 5 is number of parameter of validate function.


Why doesn't validate function work?

It's actually work. However, passport-google-oauth20 has 4 overloaded versions of validate function. Library decides the version by using number of parameter of callback (function.length). Using validate in Nestjs always use

(accessToken: string, refreshToken: string, profile: Profile, verified: VerifyCallback) => void

Because Nestjs pass to passport-google-oauth20 a callback with unknown length (..args), so the function.length is 0. With this, validate using default version above.


Solution

1. Pass a callback function into strategy constructor (walk-around)

Passing callback function as second parameter in super constructor

    export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
    // ...
    super({
      // options
    }, async() => {});
    }

then passport-google-oauth20 Strategy will receives 3 argument (option, our callback, nestjs callback). It uses 2 (options and verify), so our callback will replace Nestjs callback. Our function has explicit number of parameters so things work well.

2. Use GoogleStrategy 3rd parameter

Nesjts provides a way to define the number of parameters in callback function, so the callback.length will not be 0. We need to add callbackArity in PassportStrategy.

export class GoogleStrategy extends PassportStrategy(Strategy, 'google', 5) {
  // 5 is number of arguments in validate function
  constructor() {
    super({
      // options
    });
  }
  async validate(
    accessToken: string,
    refreshToken: string | undefined,
    params: GoogleCallbackParameters,
    profile: Profile,
    done: VerifyCallback,
  ): Promise<void> {
    // ...
    done(null, user);
  }

Upvotes: 0

Abhik Banerjee
Abhik Banerjee

Reputation: 377

Apart from the answer above by @miguel-jurado, I would like to add my two cents. In the recent version of Nest and Passport the above might lead to errors. I am using the following versions:

    "@nestjs/jwt": "^10.1.0",
    "@nestjs/passport": "^10.0.0",
    "passport": "^0.6.0",
    "passport-google-oauth20": "^2.0.0",
    "passport-jwt": "^4.0.1",

and using the above code gives me Circular Dependency error. In my case, I just needed to tweak it a little to not include passReqToCallback and so overload a different constructor. My code was:

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

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(configService: ConfigService) {
    super(
      {
        clientID: configService.get('GOOGLE_CLIENT_ID'),
        clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
        callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
        scope: ['email', 'profile', 'openid'],
      },
      async (
        accessToken: string,
        refreshToken: string,
        params: GoogleCallbackParameters,
        profile: Profile,
        done: VerifyCallback,
      ) => {
        const {expires_in, id_token} = params;
        const { name, emails, photos } = profile;
        const user = {
          email: emails[0].value,
          firstName: name.givenName,
          lastName: name.familyName,
          picture: photos[0].value,
          accessToken,
          refreshToken,
          id_token,
          expires_in
        };
        done(null, user);
      },
    );
  }
}

Upvotes: 0

Miguel Jurado
Miguel Jurado

Reputation: 29

I solved the problem by passing the callback directly in the super method as a second parameter, I do not why in the validate method it does not work, maybe it is a problem of the passportStrategy that uses nestjs.

Something like below works:

@Injectable()
    export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
      constructor() {
        super(
          {
            clientID:
              process.env.BACKEND_ENV === 'dev'
                ? googleStrategy.GOOGLE_CLIENT_ID
                : process.env.GOOGLE_CLIENT_ID,
            clientSecret:
              process.env.BACKEND_ENV === 'dev'
                ? googleStrategy.GOOGLE_CLIENT_SECRET
                : process.env.GOOGLE_CLIENT_SECRET,
            callbackURL:
              process.env.BACKEND_ENV === 'dev'
                ? googleStrategy.GOOGLE_CALLBACK_URL
                : process.env.GOOGLE_CALLBACK_URL,
            scope: ['email', 'profile', 'openid'],
            passReqToCallback: true,
          },
          async (
            req: Request,
            accessToken: string,
            refreshToken: string,
            params: GoogleCallbackParameters,
            profile: Profile,
            done: VerifyCallback,
          ): Promise<any> => {
            const { name, emails, photos } = profile;
            const user = {
              email: emails[0].value,
              firstName: name.givenName,
              lastName: name.familyName,
              picture: photos[0].value,
              accessToken,
              refreshToken,
            };
    
            done(null, user);
          },
        );
      }
    }

Upvotes: 2

Related Questions