Taofiq Aiyelabegan Taofiq is a technical writer and software engineer working on building mobile and web applications. Additionally, he likes to contribute to open source projects and exploring new topics.

Implementing secure single sign-on authentication in NestJS

10 min read 3055

Implementing Secure Sign-On in Nest.js With Google

NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. In this tutorial, we will learn how to implement a secure Google single sign-on (SSO) in the NestJS backend service and connect it to a React frontend application to authenticate users in a simple web application.

Jump ahead:

Setting up the client and server folders in NestJS

To get started, we’ll create the frontend client and backend server folders. Next, we’ll navigate to the client folder and initialize the React application by running the command npx create-react app react-nestjs in the terminal. This will create all the necessary files for our frontend app.

For the backend side, we will run the nest new server command in the terminal. This will scaffold a new NestJS project directory and populate the directory with the initial core NestJS files and supporting modules.

The folder directory should look like this:

Nest.js Project Directory

Then, we will navigate to client and run npm run start to get our frontend app up and running. This will start a local development server as localhost:3000.

Starting the development server

To begin our backend development server, we’ll navigate to the server folder and run npm run start. Because we’ve already used the localhost:3000, we will change the port for the backend to localhost:8080 in the main.ts file:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 await app.listen(8080);
}
bootstrap();

The npm run start will start the development server for us at localhost:8080 and display “Hello, World!” from the app.controller.ts file.

The app.controller.ts uses the getHello method defined in app.service.ts to return the text when the development server starts at localhost:8080. When you check the app.controller.ts, you should see this:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
 constructor(private readonly appService: AppService) {}

 @Get()
 getHello(): string {
   return this.appService.getHello();
 }

This is the result in the app.service.ts file:

import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
  getHello(): string {
   return 'Hello World!';
 }

You can read more about the controllers in NestJS here.

Creating a secure SSO project on the Google Cloud Platform

After scaffolding our app, we’ll set up a new project on the Google Cloud Platform. First, we’ll navigate to the console and create a new project by selecting Project Bar in the top left corner. Then, click New Project and enter your project details. After that, search for credentials in the search bar and select API and Services.

Then, we’ll configure the content screen by clicking the Configure Consent Screen before creating our credentials. Next, select Configure Screen and enter the app information. Next, we’ll select Save and Continue. Then, click Credentials, Create Credentials, and select OAuth client ID.

This will prompt us to a page to create an OAuth Client ID, where we will select Web application as the Application Type. Then, we can add the JavaScript origins and the redirect URIs as locahost:3000. We’ll also add a second URI with the value http://localhost. Finally, click Create to get the client ID and the client secret:

NestJS Secure Sign On With Google

Configuring React OAuth 2.0 for Google sign-on

To set up the Google Auth Library, we will use React OAuth 2.0. To get started, let’s run the command npm i @react-oauth/google in the terminal. After creating our client ID, we’ll wrap the home page with the GoogleOAuthProvider.

Then, inside App.js, we will clean up the bootstrapped code and replace it with the following:

import "./App.css";
import { GoogleOAuthProvider, GoogleLogin } from "@react-oauth/google";

function App() {
 return (
   <div className="App">
         <h1>Welcome</h1>
         <GoogleOAuthProvider      clientId="192710632585-6dvq7283tr2pug31h5i6ii07fgdeu6q3.apps.googleusercontent.com
"
         >
           <GoogleLogin
             onSuccess={async (credentialResponse) => {
             console.log(credentialResponse);
                        }}
             onError={() => {
               console.log("Login Failed");
             }}
           />
         </GoogleOAuthProvider>
   </div>
 );
}

export default App;

Here, we used GoogleOAuthProvider and the GoogleLogin APIs to implement the Google SSO functionality. Then, we logged the credential response from GoogleLogin in the console. We also passed the clientId to the GoogleOAuthProvider.

Using the clientId

Now, we’ll sign in with our email and check the console for the logged credential response. In the console, we should have a clientId response object:

Example of NestJS ClientID Response Object

Lastly, we have to store the clientId that we’re passing to GoogleOAuthProvider in an environment variable file. Because it’s a Secret ID, it should not be directly exposed to the browser or pushed to the repository alongside other code.

Next, create a .env file in the client folder, and add .env to .gitignore so the file is ignored when pushed to the repository. Inside .env, create a REACT_APP_GOOGLE_CLIENT_ID variable and pass it in the clientId raw value:

# google client id
REACT_APP_GOOGLE_CLIENT_ID=192710632585-6dvq7283tr2pug31h5i6ii07fgdeu6q3.apps.googleusercontent.com

It’s important to note that the variable name has to be prefixed with the REACT_APP keyword.

Then, in App.js, we will pass process.env.REACT_APP_GOOGLE_CLIENT_ID as the value for clientId for React to read .env:

import "./App.css";
import { GoogleOAuthProvider, GoogleLogin } from "@react-oauth/google";
function App() {
 return (
   <div className="App">
         <h1>Welcome</h1>
         <GoogleOAuthProvider
           clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}
         >
           <GoogleLogin
             onSuccess={async (credentialResponse) => {
               console.log(credentialResponse);
                         }}
             onError={() => {
               console.log("Login Failed");
             }}
           />
         </GoogleOAuthProvider>
       </div>
 );
}

export default App;

State management with Zustand for your NestJS project

We will use Zustand for our global state management. However, you can use Redux, Recoil, or any state management of your choice. To get started, run npm install zustand to install the package:

Then, we will create a hook folder and create the useStore.js store file:

import create from "zustand";

export const useStore = create((set) => ({
 // get the data from local storage
 authData: localStorage.getItem("authData")
   ? JSON.parse(localStorage.getItem("authData"))
   : null,

 setAuthData: (authData) => {
   localStorage.setItem("authData", JSON.stringify(authData));
   set({ authData });
 },
}));

Next, we’ll make an Axios POST request using the Axios.post method whenever a user logs in.

First, install Axios with npm install axios and make a Post request when selecting GoogleLogin:

import "./App.css";
import { GoogleOAuthProvider, GoogleLogin } from "@react-oauth/google";
import axios from "axios";
import { useStore } from "./hooks/useStore";

function App() {
 const setAuthData = useStore((state) => state.setAuthData);
 return (
   <div className="App">
              <h1>Welcome</h1>
         <GoogleOAuthProvider
           clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}
         >
           <GoogleLogin
             useOneTap={true}
             onSuccess={async (credentialResponse) => {
               console.log(credentialResponse);
               const { data } = await axios.post(
                 "http://localhost:8080/login",
                                );
               localStorage.setItem("AuthData", JSON.stringify(data));
               setAuthData(data);
             }}
             onError={() => {
               console.log("Login Failed");
             }}
           />
         </GoogleOAuthProvider>   
      </div>
 );
}

export default App;

Remember, we set the port for our backend as 8080, so we’ll request 8080/login later in our backend. Then, we’ll store the data from the backend server in local storage and set setAuthData to the data that our backend service will return.

Building the NestJS folder

Next, we need to set up our NestJS folder in the server we created earlier:

NestJS SSO Server Folder

To confirm that we successfully connected NestJS to the frontend app, we will return React x NestJS in app.controller.ts using the POST HTTP request method:

import { Controller, Post } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
 @Post('/login')
 login: string {
   return 'Reaxt x NestJS';
 }
}

Now, let’s log in to the frontend app and check the browser’s local storage:

Checking the NestJS Browser Storage

Once we log in successfully, we can send the response as a text. From here, we’ll create a schema and send the name, email, picture user objects.

Remember, for the connection between NestJS and React to work, we need to enable cors in the NestJS main.ts file. Otherwise, there will be an error when calling the backend endpoint:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 //enable cors
 app.enableCors({
   origin: 'http://localhost:3000',
 });
 await app.listen(8080);
}
bootstrap();

Because our port origin is localhost:3000, we will pass it to the origin in the enableCors method.

Implementing Google OAuth in NestJS

We will start with the Google Auth Library: Node.js Client on the server side. To install this package, run npm i google-auth-library and import the OAuth2Client to retrieve an access token, then refresh the token.

Next, we will initiate a new OAuth2Client instance, pass clientId, and generate the secret when setting up the Google Cloud Platform.

Again, we do not want to pass the values plainly in our code, so we need to implement .env for the server side. First, create an .env and include it in .gitignore. Then, add the two variables in the .env file:

GOOGLE_CLIENT_ID=192710632585-6dvq7283tr2pug31h5i6ii07fgdeu6q3.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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

Now, we need to load and parse .env from the project root directory. We’ll also merge key-value pairs from .env with the environment variables and store the result in a private structure accessed through the ConfigService. To do this, we will install npm i --save @nestjs/config which uses dotenv internally.



Then, we’ll import the ConfigModule from nest/config into the root AppModule and control its behavior using the .forRoot() static method:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';

@Module({
 imports: [
   ConfigModule.forRoot({
     isGlobal: true,
     envFilePath: '.env',
   }),
  ],
 controllers: [AppController],
 providers: [AppService],
})
export class AppModule {}

Now, we can read our .env variables from anywhere inside the project. Next, we’ll implement the Google Authentication in app.controller.ts:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';
import { OAuth2Client } from 'google-auth-library';

const client = new OAuth2Client(
 process.env.GOOGLE_CLIENT_ID,
 process.env.GOOGLE_CLIENT_SECRET,
);

@Controller()
export class AppController {
 @Post('/login')
 async login(@Body('token') token): Promise<any> {
   const ticket = await client.verifyIdToken({
     idToken: token,
     audience: process.env.GOOGLE_CLIENT_ID,
   });
    // log the ticket payload in the console to see what we have
   console.log(ticket.getPayload());
    }
}

Here, we pass the idToken and audience to the Body decorator, the equivalent of req.body. The audience is the clientId and the token will come from our credentialObject on the frontend.

We will pass the token in App.js as the credentialObject.credentials to pass it alongside Body:

<GoogleLogin
             useOneTap={true}
             onSuccess={async (credentialResponse) => {
               console.log(credentialResponse);
               const { data } = await axios.post(
                 "http://localhost:8080/login",
                 {
                   // pass the token as part of the req body
                   token: credentialResponse.credential,
                 }
               );
               localStorage.setItem("AuthData", JSON.stringify(data));
               setAuthData(data);
             }}
             onError={() => {
               console.log("Login Failed");
             }}
           />

When we successfully log in, we should have this in our terminal as the ticket.getPayload() data:

NestJS SSO Terminal

Saving our NestJS SSO project data to MongoDB

For this tutorial, we will use MongoDB to save users’ information in the database. To get started, sign up or log in to a MongoDB Cloud Services account. Then, create a new free-tier cluster:

Adding NestJS to the MongoDB Database

Creating the NestJS Shared Cluster

Building the user schema

After creating the cluster, connect with your application to get your MONGOURI. Copy the URI and paste it into the .env file for the server. Then, we will connect our database with our NestJS app inside the app.module.ts.

We will create a user schema by creating the user.schema.ts file. You can read more about connecting the MongoDB database in NestJS here:

MONGO_URI=mongodb+srv://Taofiq:[email protected]/?retryWrites=true&w=majority


import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
 @Prop()
 name: string;

 @Prop()
 email: string;

 @Prop()
 image: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

In our schema, we’ll send the name, email, and image props as a response when a login request is made to the Post.

Now, in the app module, we will configure the MongoDB connection:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user.schema';

@Module({
 imports: [
   ConfigModule.forRoot({
     isGlobal: true,
     envFilePath: '.env',
   }),
   MongooseModule.forRoot(process.env.MONGO_URI),
   MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
 ],
 controllers: [AppController],
 providers: [AppService],
})
export class AppModule {}

Applying the frontend secure single sign-on with data from NestJS

Moving on, we will create an app service to create new users and log existing users into the app:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './user.schema';

@Injectable()
export class AppService {
 constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
 async login({
   email,
   name,
   image,
 }: {
   email: string;
   name: string;
   image: string;
 }): Promise<any> {
   const user = await this.userModel.findOne({ email: email });
   if (!user) {
     const newUser = new this.userModel({ email, name, image });
     await newUser.save();
     return newUser;
   } else {
     console.log(user);
     return user;
   }
 }
}

Here, we’ll create a new login function. First, we’ll verify the existence of a user’s email. If they are not in the database, create a new user with the userModel. If the user already exists in the database, we will return the existing user’s name, email, and image when the endpoint is called. From here, a ticket is created and the token and audience values are passed in:

{
                   // pass the token as part of the req body
                   token: credentialResponse.credential,
                 }

Then, in our app.controller.ts, we will use login by using the token we passed in App.js. Here’s a recap of the ticket.getPayload() object data for when a user successfully logs in:

Successful NestJS SSO Ticket Object Data

Since our schema is based on name, email, and image, we can destructure these values from ticket.getPayload and use them in the login:

@Post('/login')
 async login(@Body('token') token): Promise<any> {
   const ticket = await client.verifyIdToken({
     idToken: token,
     audience: process.env.GOOGLE_CLIENT_ID,
   });
   console.log(ticket.getPayload(), 'ticket');
   const { email, name, picture } = ticket.getPayload();
   const data = await this.appService.login({ email, name, image: picture });
   return {
     data,
     message: 'success',
   };
 }

When a user tries to log in, the destructured props are taken from the ticket object data and passed to the login. If the user exists, log in the user and send the user data back to the frontend app. If the user does not exist, create new data for the user and send it back to the frontend app.

Now, we can move back to the client and create a User component that will display the user details when they log in. We will create a component folder with a User.js file and send back the name, email, and image.

We’ll use the useStore hook and render the displayed data when a user logs in.

We will also check that the User is only rendered when the data is sent from the backend server and create a Logout button using the GoogleAuth.signOut() API. When clicked, the button will be called, remove the authData from the local storage, set the setAuthData to null, and reload the window:

import React from "react";
import { useStore } from "../hooks/useStore";
import { googleLogout } from "@react-oauth/google";
import "../User.css";
const User = () => {
 const { authData, setAuthData } = useStore();
 return (
   <div className={"container"}>
     {authData && (
       <>
         <h1>{authData.data.name}</h1>
         <p>{authData.data.email}</p>
         <img src={authData.data.image} alt="profile" />

         <button
           onClick={() => {
             googleLogout();
             localStorage.removeItem("AuthData");
             setAuthData(null);
             window.location.reload();
           }}
           className={"button"}
         >
           Logout
         </button>
       </>
     )}
   </div>
 );
};

export default User;

Now, we can use Logout in App.js:

import "./App.css";
import { GoogleOAuthProvider, GoogleLogin } from "@react-oauth/google";
import axios from "axios";
import { useStore } from "./hooks/useStore";
import User from "./components/User";

function App() {
 const setAuthData = useStore((state) => state.setAuthData);
 return (
   <div className="App">
     {!useStore((state) => state.authData) ? (
       <>
         <h1>Welcome</h1>
         <GoogleOAuthProvider
           clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}
         >
           <GoogleLogin
             useOneTap={true}
             onSuccess={async (credentialResponse) => {
               console.log(credentialResponse);
               const { data } = await axios.post(
                 "http://localhost:8080/login",
                 {
                   // pass the token as part of the req body
                   token: credentialResponse.credential,
                 }
               );
               localStorage.setItem("AuthData", JSON.stringify(data));
               setAuthData(data);
             }}
             onError={() => {
               console.log("Login Failed");
             }}
           />
         </GoogleOAuthProvider>
       </>
     ) : (
       <>
         <h1>React x Nestjs Google Sign in</h1>
         <User />
       </>
     )}
   </div>
 );
}

export default App;

Here, we’ll check if authData was sent from the backend server with useStore hook. If there is no data, it will render a “Welcome” text and the Google login button. However, if the user has logged in, we will render React x NestJs Google sign in and the User Details.


More great articles from LogRocket:


Now, let’s start the whole process and see it in action:

Example of NestJS SSO Welcome in Google

When we log in, we have the text and the user details:

Example of NestJS Secure SSO

Next, we can check our database to see if the user is saved:

Checking the React-NestJS Database

Success! Our user is in our database. In summary, we successfully logged in using Google sign-in, retrieved the user data from NestJS, and displayed it on the React frontend app once the user signed in.

Now, when we log out, we’ll see this:

NestJS SSO Log Out

Conclusion

In this tutorial, we learned how to implement a secure Google sign-on in a NestJS project. We connected it to a basic frontend React application while saving the user information in a database. You can find the complete code for this project in my repository here. Happy coding!

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Taofiq Aiyelabegan Taofiq is a technical writer and software engineer working on building mobile and web applications. Additionally, he likes to contribute to open source projects and exploring new topics.

Leave a Reply