2

In my full-stack project(Nest.js + React), I implemented google login using passport-google as below.

import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {
    constructor(
        private authService: AuthService,
    ) {}

    ...

    @Get('google')
    @UseGuards(GoogleAuthGuard)
    async googleAuth(@Req() req) {}
  
    @Get('google/callback')
    @UseGuards(GoogleAuthGuard)
    async googleAuthRedirect(@Req() req) {
        return this.authService.socialLogin(req);
    }
}

When I access to http://localhost:3000/auth/google with Chrome Browser, all the processes, including login and registration of new members, work well.

But on frontend project(React), below code not working.

axios.get('http://localhost:3000/auth/google')

So I tried to use 'react-google-login' as below:

<GoogleLogin 
    clientId={GOOGLE_OAUTH.GOOGLE_CLIENT_ID}
    buttonText="Log In with Google Account"
    onSuccess={result => onGoogleLogin(result)}
    onFailure={result => console.log(result)}
    cookiePolicy={'single_host_origin'}
/>

but I couldn't understand the flow of Google login at the frontend and backend and don't know how to implement the function "onGoogleLogin".

How can I fix it?

Swimmie
  • 39
  • 3

2 Answers2

2

Hi, change your front-end into:

<Button
   color="secondary"
   startIcon={<GoogleIcon />}
   onClick={googleLogin}
   fullWidth
   variant="outlined"
   size="large"
>
     Login with Google
</Button>

and add googleLogin func as below:

  const googleLogin = async () => {
    try {
      window.open(`http://localhost:3001/auth/google-logins/${from.replaceAll('/', '@')}`, "_self");
    } catch (ex) {
      console.log(ex)
    }
  }

change your route file like this:

<Route path="google-oauth-success-redirect">
     <Route path=":accessToken/:refreshToken/:from" element={<GoogleOAuthSuccessRedirect />} />
</Route>

and add GoogleOAuthSuccessRedirect component:

import React, { useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom';
import { setAuthTokens } from 'redux/features/auth/authSlice';
import { useAppDispatch } from 'redux/hooks';

type Props = {}

const GoogleOAuthSuccessRedirect = (props: Props) => {

    let { accessToken, refreshToken, from } = useParams();
    const navigate = useNavigate();
    const dispatch = useAppDispatch()

    useEffect(() => {
        if (from && accessToken && refreshToken) {
            dispatch(setAuthTokens({ accessToken, refreshToken }))
            navigate('/' + from, { replace: true });
        }
    }, [accessToken, dispatch, from, navigate, refreshToken])


    return (
        <div>Loading...</div>
    )
}

export default GoogleOAuthSuccessRedirect

turn to the backend side of your app and change that like this:

on controller:

@Get('google-logins/:from')
  @UseGuards(GoogleOauthGuard)
  async googleLogin(@Req() req: Request) {
  }

  @Get('google/callback')
  @UseGuards(GoogleOauthGuard)
  async googleLoginCallback(
    @Req() req: Request,
    @Res() res: Response,
  ) {
    const auth = await this.authService.login(req.user);
    res.redirect(`http://localhost:3000/google-oauth-success-redirect/${auth.accessToken}/${auth.refreshToken}${req.params.from}`)
  }

on google strategy:

authenticate(req: any, options: any) {

    if (!options?.state) {
      options = { ...options, state: req.params.from }
    } 
    
    return super.authenticate(req, options)
  }

and google oauth guard as below:

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthGuard, IAuthModuleOptions } from '@nestjs/passport';
import { Request, Response } from 'express';

@Injectable()
export class GoogleOauthGuard extends AuthGuard('google') {
  constructor(private configService: ConfigService) {
    super({
      // accessType: 'offline',
      // response_type: "code",
      // display: 'popup',
      // approvalPrompt: 'auto',
      prompt: 'select_account', //"consent"
    });
  }

  async canActivate(context: ExecutionContext) {
    try {
      const request = context.switchToHttp().getRequest() as Request;
      const from = (request.query.state as string)?.replace(/\@/g, '/')
      // response.setHeader('X-Frame-Options', 'SAMEORIGIN');
      // await super.logIn(request) //to enabling session / we dont need it  

      const activate = (await super.canActivate(context)) as boolean;
      request.params.from = from
      return activate;
    } catch (ex) {
      throw ex
    }
  }
}

at the end you could access complete source code of my app that implemented with react typescript (redux toolkit rtk Query) and nestjs included of google oauth2 flow with passport.js. resource

mehdi parastar
  • 737
  • 4
  • 13
  • 29
  • I spent A DAY searching for what after I had been redirected to the redirect (callback) endpoint, I need front-end UI instead of a JSON response on my browser, and finally here is the answer, use `res.redirect()` with query parameters to redirect browser back to the front-end, that's all what I need!!! Really, people making tutorials should at least explain the complete workflow in the real world instead of just returning a JSON object back to the browser and say "that's it". – Kyung Lee Jul 20 '23 at 08:48
  • @KyungLee wondering if there's any security issue when expose the jwt in url? – Alex Lam Jul 22 '23 at 05:38
  • @Alex Lam when you sign in to a site in the JWT way, the response will be sent back to your browser in plaintext and saved in cookie or localStorage, users can always see the token by opening up the devtool, so I don't think that's gonna be a problem given it expires after a while anyway. If you don't want to expose the tokon in the browser address bar, you can clean the token from address bar in your frontend code right after the redirection. – Kyung Lee Jul 22 '23 at 16:20
  • 1
    @KyungLee Thanks for your sharing. I'm working on this recently. I found some articles suggesting the more secure way is to write the token directly to cookie in backend after it validated with Google OAuth. Wondering if this approach is better? – Alex Lam Jul 25 '23 at 04:11
  • 1
    Yeah, I believe that's a better way or more canonical way, but it's stored in plaintext in the cookie so for the security aspect I don't see much differences. – Kyung Lee Jul 25 '23 at 07:10
0

If you are using passport, you cannot do an ajax request to call the backend api which is /auth/google in your app instead of doing an ajax call you can just put an a tag in your frontend and redirect it to your backend and redirect it back to your frontend after that you might want to save the cookie/session also create another guard to check is there a user already sign-in or not

you can check my repository i'll linked it below and im using nestjs and nextjs

BE : https://github.com/Axselll/auth-nestjs

FE : https://github.com/Axselll/i-hate-fe

SPOILER ALERT you will run into some annoying, i don't know how to call it a bug or something. if you deploy your app on production. check out RFC6265 just google it

Cheers

Axselll
  • 1
  • 1