8 min read

Authenticating users with Google

Authenticating users with emails and passwords is a valid and common approach. However, a convenient alternative is to shift this responsibility to a third…

July 26, 2021

Authenticating users with emails and passwords is a valid and common approach. However, a convenient alternative is to shift this responsibility to a third party. In this article, we look into adding Google authentication to a NestJS project.

We can achieve the above because Google uses the OAuth (Open Authorization) framework. With it, Google users can grant access to some of their data to an application – in this case, us. In this series, we’ve already implemented a way to register users. Therefore, this article aims to implement an additional way of registering users with Google that can work besides authenticating with a password and an email. Because of that, in this article, we won’t be using the popular passport-google-oauth20 library.

Registering an application#

The first step in implementing authentication with Google is registering an application in the Google Cloud Platform dashboard.

When we have a project, we need to set up the OAuth consent screen.

When configuring it, we need to set up some basic information about our application. For example, if we would like to deploy our application and use it outside of localhost, we would need to register our domain.

For testing purposes, we also need to specify a list of users that are allowed to use our application. If we would like our application to be available to any user, we need to submit our app for verification.

After sorting out the above, we need to generate OAuth Client ID credentials for our application in the credentials dashboard.

We could also set the redirect URL, but in this article we use a different approach that doesn’t use it.

Above, in the authorized JavaScript origins, we put the URL of our frontend application. We also need to specify the redirect URL. Google will redirect the user to it after successful authentication.

Finalizing the process of creating the credentials gives us the client ID and the client secret. We need to save them in the environment variables.

app.module.ts#
import { Module } from "@nestjs/common"
import { ConfigModule } from "@nestjs/config"
import * as Joi from "@hapi/joi"
@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        GOOGLE_AUTH_CLIENT_ID: Joi.string().required(),
        GOOGLE_AUTH_CLIENT_SECRET: Joi.string().required(), // ...
      }),
    }), // ...
  ], // ...
})
export class AppModule {}
.env#
GOOGLE_AUTH_CLIENT_ID=...
GOOGLE_AUTH_CLIENT_SECRET=...
# ...

The frontend side#

To implement authentication with Google in our application, we need to allow the users to type in their Google credentials. A straightforward way to do that in React is to use the react-google-login library.

In this simple example we use Create React App.

First, we need to add some environment variables.

.env#
PORT=8080
REACT_APP_GOOGLE_AUTH_CLIENT_ID=...
REACT_APP_API_URL=http://localhost:3000
react-app-env.d.ts#
namespace NodeJS {
  interface ProcessEnv {
    REACT_APP_GOOGLE_AUTH_CLIENT_ID: string;
    REACT_APP_API_URL: string;
  }
}

Once we have the above, we can use the react-google-login library.

GoogleButton.tsx#
import React from 'react';
import GoogleLogin from 'react-google-login';
import useGoogleAuthentication from "./useGoogleAuthentication";
 
function GoogleButton() {
  const clientId = process.env.REACT_APP_GOOGLE_AUTH_CLIENT_ID;
  const { handleSuccess } = useGoogleAuthentication();
 
  return (
    <GoogleLogin
      clientId={clientId}
      buttonText="Log in"
      onSuccess={handleSuccess}
    />
  );
}
 
export default GoogleButton;

Clicking on the above button causes a popup to show.

To change the above behavior, we could pass uxMode="redirect" to the GoogleLogin component. This would cause a full redirect instead of opening a popup.

When the user successfully authenticates, the onSuccess callback is invoked.

useGoogleAuthentication.tsx#
import { GoogleLoginResponse, GoogleLoginResponseOffline } from "react-google-login"
 
function useGoogleAuthentication() {
  const handleSuccess = (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
    if ("accessToken" in response) {
      const accessToken = response.accessToken
      fetch(`${process.env.REACT_APP_API_URL}/google-authentication`, {
        method: "POST",
        body: JSON.stringify({
          token: accessToken,
        }),
        headers: {
          "Content-Type": "application/json",
        },
      })
    }
  }
  return {
    handleSuccess,
  }
}
export default useGoogleAuthentication

Above, we take the accessToken from the response and send it to our NestJS API.

Implementing Google authentication with NestJS#

The last step in authenticating our users is receiving the accessToken from Google and logging the user into our system.

googleAuthentication.controller.ts#
import {
  Controller,
  Post,
  ClassSerializerInterceptor,
  UseInterceptors,
  Body,
  Req,
} from "@nestjs/common"
import TokenVerificationDto from "./tokenVerification.dto"
import { GoogleAuthenticationService } from "./googleAuthentication.service"
import { Request } from "express"
@Controller("google-authentication")
@UseInterceptors(ClassSerializerInterceptor)
export class GoogleAuthenticationController {
  constructor(private readonly googleAuthenticationService: GoogleAuthenticationService) {}
  @Post()
  async authenticate(@Body() tokenData: TokenVerificationDto, @Req() request: Request) {
    const { accessTokenCookie, refreshTokenCookie, user } =
      await this.googleAuthenticationService.authenticate(tokenData.token)
    request.res.setHeader("Set-Cookie", [accessTokenCookie, refreshTokenCookie])
    return user
  }
}

Above, we expect the frontend to call the /google-authentication endpoint with the access token.

tokenVerificationDto.ts#
import { IsString, IsNotEmpty } from "class-validator"
export class TokenVerificationDto {
  @IsString()
  @IsNotEmpty()
  token: string
}
export default TokenVerificationDto

The final part of the implementation is the authenticate method.

googleAuthentication.service.ts#
import { Injectable, UnauthorizedException } from "@nestjs/common"
import { UsersService } from "../users/users.service"
import { ConfigService } from "@nestjs/config"
import { google, Auth } from "googleapis"
import { AuthenticationService } from "../authentication/authentication.service"
import User from "../users/user.entity"
@Injectable()
export class GoogleAuthenticationService {
  oauthClient: Auth.OAuth2Client
  constructor(
    private readonly usersService: UsersService,
    private readonly configService: ConfigService,
    private readonly authenticationService: AuthenticationService,
  ) {
    const clientID = this.configService.get("GOOGLE_AUTH_CLIENT_ID")
    const clientSecret = this.configService.get("GOOGLE_AUTH_CLIENT_SECRET")
    this.oauthClient = new google.auth.OAuth2(clientID, clientSecret)
  }
  async authenticate(token: string) {
    const tokenInfo = await this.oauthClient.getTokenInfo(token)
    const email = tokenInfo.email
    try {
      const user = await this.usersService.getByEmail(email)
      return this.handleRegisteredUser(user)
    } catch (error) {
      if (error.status !== 404) {
        throw new error()
      }
      return this.registerUser(token, email)
    }
  } // ...
}

Registering new users#

The crucial part is that the users can be signed up into our system at this point, but they don’t have to be. Therefore, we need to handle both cases. Let’s start with registering the user.

googleAuthentication.service.ts#
async registerUser(token: string, email: string) {
  const userData = await this.getUserData(token);
  const name = userData.name;
 
  const user = await this.usersService.createWithGoogle(email, name);
 
  return this.handleRegisteredUser(user);
}

In our API, we require the user to provide a name. We can get this data from the Google API. Unfortunately, the googleapis library requires us to do that in a way that is a bit odd.

googleAuthentication.service.ts#
async getUserData(token: string) {
  const userInfoClient = google.oauth2('v2').userinfo;
 
  this.oauthClient.setCredentials({
    access_token: token
  })
 
  const userInfoResponse = await userInfoClient.get({
    auth: this.oauthClient
  });
 
  return userInfoResponse.data;
}

When the users are not registered yet, we add them to our database with the createWithGoogle method.

googleAuthentication.service.ts#
import { Injectable } from "@nestjs/common"
import { InjectRepository } from "@nestjs/typeorm"
import { Repository } from "typeorm"
import User from "./user.entity"
import StripeService from "../stripe/stripe.service"
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    private stripeService: StripeService,
  ) {}
  async createWithGoogle(email: string, name: string) {
    const stripeCustomer = await this.stripeService.createCustomer(name, email)
    const newUser = await this.usersRepository.create({
      email,
      name,
      isRegisteredWithGoogle: true,
      stripeCustomerId: stripeCustomer.id,
    })
    await this.usersRepository.save(newUser)
    return newUser
  } // ...
}

To handle it properly, let’s make some changes to the UserEntity:

user.entity.ts#
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"
import { Exclude } from "class-transformer"
@Entity()
class User {
  @PrimaryGeneratedColumn()
  public id: number
  @Column({ unique: true })
  public email: string
  @Column()
  public name: string
  @Column({ nullable: true })
  @Exclude()
  public password?: string
  @Column({ default: false })
  public isRegisteredWithGoogle: boolean // ...
}
export default User

Please notice that the password is now nullable, because we don’t need it for users authenticated with Google. It would be a good idea to modify the existing code usef for verifying the user’s password to account for this change.

Returning the data of the user#

When the users are registered, we need to generate cookies for them.

googleAuthentication.service.ts#
async getCookiesForUser(user: User) {
  const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(user.id);
  const {
    cookie: refreshTokenCookie,
    token: refreshToken
  } = this.authenticationService.getCookieWithJwtRefreshToken(user.id);
 
  await this.usersService.setCurrentRefreshToken(refreshToken, user.id);
 
  return {
    accessTokenCookie,
    refreshTokenCookie
  }
}
 
async handleRegisteredUser(user: User) {
  if (!user.isRegisteredWithGoogle) {
    throw new UnauthorizedException();
  }
 
  const {
    accessTokenCookie,
    refreshTokenCookie
  } = await this.getCookiesForUser(user);
 
  return {
    accessTokenCookie,
    refreshTokenCookie,
    user
  }
}

Thanks to implementing all of the above, the /google-authentication handles both new and returning users.

To see the whole picture more accurately, check out the full code for the GoogleAuthenticationService.

Summary#

In this article, we’ve implemented a way for our users to authenticate with Google. Moreover, we’ve done it in a way that integrates with our existing system. To do that, we register users into our own database instead of relying upon Google. Thanks to doing so, we don’t have to make significant changes to the existing code that takes care of registering users with the email and the password.

Authenticating users with Google | NestJS.io