Nest.js Tutorial

Authenticating users with Google

Marcin Wanago
JavaScriptNestJSTypeScript

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
1import { Module } from '@nestjs/common';
2import { ConfigModule } from '@nestjs/config';
3import * as Joi from '@hapi/joi';
4 
5@Module({
6  imports: [
7    ConfigModule.forRoot({
8      validationSchema: Joi.object({
9        GOOGLE_AUTH_CLIENT_ID: Joi.string().required(),
10        GOOGLE_AUTH_CLIENT_SECRET: Joi.string().required(),
11        // ...
12      })
13    }),
14   // ...
15  ],
16  // ...
17})
18export class AppModule {}
.env
1GOOGLE_AUTH_CLIENT_ID=...
2GOOGLE_AUTH_CLIENT_SECRET=...
3# ...

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
1PORT=8080
2REACT_APP_GOOGLE_AUTH_CLIENT_ID=...
3REACT_APP_API_URL=http://localhost:3000
react-app-env.d.ts
1namespace NodeJS {
2  interface ProcessEnv {
3    REACT_APP_GOOGLE_AUTH_CLIENT_ID: string;
4    REACT_APP_API_URL: string;
5  }
6}

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

GoogleButton.tsx
1import React from 'react';
2import GoogleLogin from 'react-google-login';
3import useGoogleAuthentication from "./useGoogleAuthentication";
4 
5function GoogleButton() {
6  const clientId = process.env.REACT_APP_GOOGLE_AUTH_CLIENT_ID;
7  const { handleSuccess } = useGoogleAuthentication();
8 
9  return (
10    <GoogleLogin
11      clientId={clientId}
12      buttonText="Log in"
13      onSuccess={handleSuccess}
14    />
15  );
16}
17 
18export 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
1import { GoogleLoginResponse, GoogleLoginResponseOffline } from 'react-google-login';
2 
3function useGoogleAuthentication() {
4  const handleSuccess = (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
5    if ('accessToken' in response) {
6      const accessToken = response.accessToken;
7 
8      fetch(`${process.env.REACT_APP_API_URL}/google-authentication`, {
9        method: 'POST',
10        body: JSON.stringify({
11          token: accessToken
12        }),
13        headers: {
14          'Content-Type': 'application/json'
15        },
16      });
17    }
18  }
19 
20  return {
21    handleSuccess,
22  }
23}
24 
25export 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
1import {
2  Controller,
3  Post,
4  ClassSerializerInterceptor,
5  UseInterceptors,
6  Body,
7  Req,
8} from '@nestjs/common';
9import TokenVerificationDto from './tokenVerification.dto';
10import { GoogleAuthenticationService } from './googleAuthentication.service';
11import { Request } from 'express';
12 
13@Controller('google-authentication')
14@UseInterceptors(ClassSerializerInterceptor)
15export class GoogleAuthenticationController {
16  constructor(
17    private readonly googleAuthenticationService: GoogleAuthenticationService
18  ) {
19  }
20 
21  @Post()
22  async authenticate(@Body() tokenData: TokenVerificationDto, @Req() request: Request) {
23    const {
24      accessTokenCookie,
25      refreshTokenCookie,
26      user
27    } = await this.googleAuthenticationService.authenticate(tokenData.token);
28 
29    request.res.setHeader('Set-Cookie', [accessTokenCookie, refreshTokenCookie]);
30 
31    return user;
32  }
33}

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

tokenVerificationDto.ts
1import { IsString, IsNotEmpty } from 'class-validator';
2 
3export class TokenVerificationDto {
4  @IsString()
5  @IsNotEmpty()
6  token: string;
7}
8 
9export default TokenVerificationDto;

The final part of the implementation is the authenticate method.

googleAuthentication.service.ts
1import { Injectable, UnauthorizedException } from '@nestjs/common';
2import { UsersService } from '../users/users.service';
3import { ConfigService } from '@nestjs/config';
4import { google, Auth } from 'googleapis';
5import { AuthenticationService } from '../authentication/authentication.service';
6import User from '../users/user.entity';
7 
8@Injectable()
9export class GoogleAuthenticationService {
10  oauthClient: Auth.OAuth2Client;
11  constructor(
12    private readonly usersService: UsersService,
13    private readonly configService: ConfigService,
14    private readonly authenticationService: AuthenticationService
15  ) {
16    const clientID = this.configService.get('GOOGLE_AUTH_CLIENT_ID');
17    const clientSecret = this.configService.get('GOOGLE_AUTH_CLIENT_SECRET');
18 
19    this.oauthClient = new google.auth.OAuth2(
20      clientID,
21      clientSecret
22    );
23  }
24 
25  async authenticate(token: string) {
26    const tokenInfo = await this.oauthClient.getTokenInfo(token);
27 
28    const email = tokenInfo.email;
29 
30    try {
31      const user = await this.usersService.getByEmail(email);
32 
33      return this.handleRegisteredUser(user);
34    } catch (error) {
35      if (error.status !== 404) {
36        throw new error;
37      }
38 
39      return this.registerUser(token, email);
40    }
41  }
42 
43  // ...
44}

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
1async registerUser(token: string, email: string) {
2  const userData = await this.getUserData(token);
3  const name = userData.name;
4 
5  const user = await this.usersService.createWithGoogle(email, name);
6 
7  return this.handleRegisteredUser(user);
8}

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
1async getUserData(token: string) {
2  const userInfoClient = google.oauth2('v2').userinfo;
3 
4  this.oauthClient.setCredentials({
5    access_token: token
6  })
7 
8  const userInfoResponse = await userInfoClient.get({
9    auth: this.oauthClient
10  });
11 
12  return userInfoResponse.data;
13}

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

googleAuthentication.service.ts
1import { Injectable } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository } from 'typeorm';
4import User from './user.entity';
5import StripeService from '../stripe/stripe.service';
6 
7@Injectable()
8export class UsersService {
9  constructor(
10    @InjectRepository(User)
11    private usersRepository: Repository<User>,
12    private stripeService: StripeService
13  ) {}
14 
15  async createWithGoogle(email: string, name: string) {
16    const stripeCustomer = await this.stripeService.createCustomer(name, email);
17 
18    const newUser = await this.usersRepository.create({
19      email,
20      name,
21      isRegisteredWithGoogle: true,
22      stripeCustomerId: stripeCustomer.id
23    });
24    await this.usersRepository.save(newUser);
25    return newUser;
26  }
27 
28  // ...
29}
Above, you can see that we add the user to Stripe. If you want to know more, check out API with NestJS #36. Introduction to Stripe with React

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

user.entity.ts
1import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2import { Exclude } from 'class-transformer';
3 
4@Entity()
5class User {
6  @PrimaryGeneratedColumn()
7  public id: number;
8 
9  @Column({ unique: true })
10  public email: string;
11  
12  @Column()
13  public name: string;
14 
15  @Column({ nullable: true })
16  @Exclude()
17  public password?: string;
18 
19  @Column({ default: false })
20  public isRegisteredWithGoogle: boolean;
21  
22  // ...
23}
24 
25export 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.

If you want to know more about this approach, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
googleAuthentication.service.ts
1async getCookiesForUser(user: User) {
2  const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(user.id);
3  const {
4    cookie: refreshTokenCookie,
5    token: refreshToken
6  } = this.authenticationService.getCookieWithJwtRefreshToken(user.id);
7 
8  await this.usersService.setCurrentRefreshToken(refreshToken, user.id);
9 
10  return {
11    accessTokenCookie,
12    refreshTokenCookie
13  }
14}
15 
16async handleRegisteredUser(user: User) {
17  if (!user.isRegisteredWithGoogle) {
18    throw new UnauthorizedException();
19  }
20 
21  const {
22    accessTokenCookie,
23    refreshTokenCookie
24  } = await this.getCookiesForUser(user);
25 
26  return {
27    accessTokenCookie,
28    refreshTokenCookie,
29    user
30  }
31}
Above you can see that we generate refresh tokens. To read more about it, check out API with NestJS #13. Implementing refresh tokens using JWT

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.