Next x Nest - Using Supabase & Google OAuth in NestJS

Next x Nest - Using Supabase & Google OAuth in NestJS

...and making it work with your NextJS + NextAuth app.

Shall we pick up right where we left off from? The last article in this series was cut short due to excess length. We covered how to use Next Auth and Supabase with Google OAuth provider in NextJS in the last article. In this article, we will:

  1. Create a Backend in NestJS.

  2. Integrate Supabase into it using Prisma.

  3. Use a Passport to Integrate Google OAuth Strategy for Authentication (AuthN).

  4. Explore how to simultaneously use JWT Strategy in NestJS for Authorization (AuthZ).

  5. Make our NextJS frontend interact with the NestJS backend.

Before we proceed, I’d like to state that the Code for this mini-project is open-sourced in this Github Repo:

Housekeeping

We first need to install Passport, Passport’s Google OAuth & JWT Strategy, and NestJS-specific Passport wrapper. For this reason, run the npm i passport passport-jwt passport-google-oauth20 @nestjs/jwt @nestjs/passport. This will install the dependencies. Next, we need to install the dev dependencies. Running npm i -D @types/passport-google-oauth20 @types/passport-jwt will take care of that.

Also, we will be using environment variables in our NestJS backend project. For this, we need to install @nestjs/config and its dependencies. Run npm i @nestjs/config joi. For good measure, we will also be adding health checks to our backend. For this, we will use @nestjs/terminus. Unfortunately, that does not come built-in so as to make things lighter. We need to run npm i @nestjs/terminus. We will cover adding health checks in the last section of this article once we are done integrating the DB.

Using Environment variables in NestJS

Just using environment variables is as easy as importing ConfigModule from @nestjs/config and adding it to our app.module.ts. But with NestJS, we can do so much more. NestJS allows us to check the environment file and make sure that certain env variables exist before we start the app. That’s where joi comes in.

While it might make more sense to some to create a new DynamicModule to wrap the ConfigModule, we are going to stick to the simpler solution and add our configuration to the ConfigModule all inside the AppModule. Our app.module.ts file would look something like this right about now.

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

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DATABASE_URL: Joi.string().required(),
        DIRECT_URL: Joi.string().required(),
        GOOGLE_CLIENT_ID: Joi.string().required(),
        GOOGLE_CLIENT_SECRET: Joi.string().required(),
        GOOGLE_CALLBACK_URL: Joi.string().required(),
        APP_JWT_SECRET: Joi.string().required()
      }),
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

In the above code, we import ConfigModule inside AppModule. We tell NestJs that we want the ConfigModule (and in effect, the ConfigService) to be made available globally in the app using isGlobal: true. This will save us from adding the ConfigModule to every module in our NestJS backend app. Next, we define the schema of our env file. We tell NestJS that our ENV file should contain the variables with the conditions specified inside the validationSchema property. We use Joi to create a type-checker here. This is similar to Zod or Yup in the frontend.

DATABASE_URL & DIRECT_URL are for Supabase. GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_CALLBACK_URL can be obtained from the GCP console. This was covered in the previous article. These env variables will be used Google OAuth using Passport. APP_JWT-SECRET is the secret we will use for minting and managing the authZ JWT token. With this schema defined, our backend app won’t start unless we have a .env will all the values marked as required() in the validation schema.

This completes our housekeeping part. We will now dive into the major topics of this article.

Adding Passport and Google OAuth to NestJS

At this point, we already have a NestJS backend project setup in our Monorepo. We are able to run it in dev mode using Turborepo. We won’t directly get into the nitty-gritty details of creating a Backend for the Kanban Board. First, we will add AuthN and AuthZ to our backend app. The backend creation would come in the next article where we would build a GraphQL backend for our Kanban Board. By the time that article concludes, we would have a NestJS backend using GraphQL with authentication.

We need to import the PassportModule and JwtModule into our App Module. This will register them and tell NestJs that every time we use Passport or its corresponding functionality, NestJS can pick them up from here. Our app.module.ts file would now look like this:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DATABASE_URL: Joi.string().required(),
        DIRECT_URL: Joi.string().required(),
        GOOGLE_CLIENT_ID: Joi.string().required(),
        GOOGLE_CLIENT_SECRET: Joi.string().required(),
        GOOGLE_CALLBACK_URL: Joi.string().required(),
        APP_JWT_SECRET: Joi.string().required()
      }),
    }),
    PassportModule,
    JwtModule.register({
      global: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService, GoogleStrategy, JwtStrategy],
})
export class AppModule {}

Creating Google OAuth Strategy & Guard in Nest

Next, we move towards making a Google Login for our NestJS backend. The obvious question here could be “Why create a NestJS login if Login is already handled by NextJS & Next Auth” – Well the answer to that is to make sure someone can access our backend independent of the frontend. We will have Google OAuth-based Login on the backend and just as we did on the frontend, we will use the Authentication to get an id_token and an access_token from Google. This will complete the AuthN part. For the AuthZ part, we will mint a custom JWT (let’s call that auth_token, similar to what we did for the frontend. The auth_token is what we will check for in our other routes through the Guard we will make in the next section. This AuthZ token will be interoperable with the Frontend.

Firstly, we need to create a Strategy. The heavy lifting is really done by NestJs and Passport. We just need to assemble the stuff. We will use the clientID, clientSecret values from what we got from the GCP console in the previous article. callbackURL in our case would be localhost:3001/auth/google-redirect. After authentication, Google will redirect to this route on our backend with all the Google User profile data. We will need to create this route. It will be in our AuthModule covered below. The code below shows the contents of our strategy. We will call this file google.strategy.ts and place it in a new directory called strategies inside the src folder of the backend folder. Keep in mind that we need to pass the same names for the strategy and the guard for that strategy. So for instance, in the code, below at line 12, we tell Passport and Nest to use the Google Strategy and to call this whole strategy-guard collection as google.

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 {
          id,
          name,
          emails,
          photos,
          _json: { email_verified },
        } = profile;
        const user = {
          providerAccountId: id,
          email: emails[0].value,
          email_verified,
          firstName: name.givenName,
          lastName: name.familyName,
          picture: photos[0].value,
          accessToken,
          refreshToken,
          id_token,
          expires_in,
        };
        done(null, user);
      },
    );
  }
}

In the code above, apart from the values from GCP console, we tell Google OAuth that we want the email, profile, and conformance with the OpenID standard (and the related data). In the async() method, Google will send all the profile data. We need a select few things to create a user that would conform to the User table in our Supabase instance. We are picking the expires_in and id_token passed in params along with the id, name, emails, and photos from profile parameter. We also pick the emails_verified found in the _json property of the returned Google user profile. Between lines 36 and 47, we construct the user object with what we need on the Supabase table and invoke the done callback to signify that the first step of the authN is complete.

For the http-google-oauth.guard.ts file, we just need to add the below code to get it done. This guard below is where our user authentication starts. Keep in mind that we need to pass in the google key to the AuthGuard class at line 5.

import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from 'src/decorators';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const isGoogleLogin = this.reflector.getAllAndOverride<boolean>("google-login", [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic || isGoogleLogin) {
      return true;
    }
    return super.canActivate(context);
  }
}

Our flow would, thus, consist of the user going to the login link. The auth controller from AuthModule will get invoked at this link. This controller will have this guard on it. The user would be re-directed to Google Login page. On completion, the user will be redirected to our app at the callbackURL specified. At the callbackURL, the controller from AuthModule will use the Google strategy defined above to get the data, check if a user with that email exists in our database. If such a user exists then mint the user a auth_token. If it’s a new user, then save the user in the DB and then mint a JWT. Put the above guard in a separate folder called guards at the same level as that of strategies.

We will need to import the GoogleStrategy into our app.module.ts file. This we will cover in the later sections.

Creating a JWT Auth Strategy & Guard in NestJs

After the Google Strategy, it's time for the JWT strategy. In the same strategies folder as above, create a file called jwt.strategy.ts and paste the code below. Explanations follow.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get("APP_JWT_SECRET"),
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

Between lines 8 and 14, we define the constructor. Here we import the configService. We pick the APP_JWT_SECRET from our env variables and pass that on as the JWT encryption/decryption key through the secretOrKey arg to super(). We tell Passport that if the JWT has expired then it should not be allowed and also to extract the JWT from the headers at lines 11 and 10 respectively. This means that the JWT auth_token needs to be sent through the authorization header key and needs to be in a format like Bearer <JWT Token>.

The validate() function receives the decoded JWT details and we just restructure it a bit before returning. The return from this function gets populated into an user object inside our HTTP request. This completes the Strategy. Note how we pass the ‘jwt` here.

Next, in the guards folder create a file called http-jwt-auth.guard.ts. This file will contain the code below. Note how similar to the strategy above, we pass the same jwt to AuthGuard class thereby telling NestJS to associate this guard with the strategy of the same name.

import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from 'src/decorators';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const isGoogleLogin = this.reflector.getAllAndOverride<boolean>("google-login", [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic || isGoogleLogin) {
      return true;
    }
    return super.canActivate(context);
  }
}

We pass in a reflector because we need to access the request here. The return from canActivate() determines if a request to invoke a controller can actually activate the code controller code or be rejected. If this function returns true, then the request is allowed to pass through. Otherwise, it is stopped.

We have used something new here. It’s the IS_PUBLIC_KEY. We will define this in the next section. Basically, for public routes, this key will be set to true in the request metadata for public routes through a controller decorator. Similarly, for the Google Login route, we will allow the function to return true. So, if we find that the incoming request is for a public route or for our login route then it is allowed otherwise it needs to go through the JWT Auth pipeline to find out if it's coming from an authenticated user who is authorized to access our routes.

Decorators for Routes in NestJS

If you have a basic knowledge of NestJS, you’d know what decorator tags are. We will need to define custom decorators to pick up the decoded JWT user object from requests and for public routes. Create a decorators folder on the same level as strategies and guards and then follow the explanations below.

Public Route Decorator

Public routes will have their controllers attached to this decorator so that the user can just pass through. The Code content for the public.decorator.ts is really simple for this as shown below. We just set the Metadata of the request so that the isPublic is set to true once the user request enters the NestJS pipeline.

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

HTTP Auth User Decorator

We need to create a @HttpUser decorator which will pick the user (coming from the decoded JWT) object from the HTTP request. This will be used for authenticated routes that require the logged-in user details. For this, create a file called http-user.decorator.ts and paste the code below.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const HttpUser = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

In the above code, we get the ExecutionContext. This allows us access to the request and response objects in NestJS (though we only need to use the request part here). At line 5 we get the request and then at line 6 send the user. We could have used the data argument for custom operations. But that’s not needed here.

GraphQL Auth User Decorator

Similar to the decorator for extracting decoded user from HTTP requests, we need one for GraphQL requests. We need to put the code below inside graphql-user.decorator.ts. The code below does exactly what the decorator above does but for GraphQL requests.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const GraphQLUser = createParamDecorator(
  (_data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req.user;
  },
);

Adding Supabase to Nest

Now, we will add Supabase to our NestJS project. We will be using Prisma to connect to our Supabase Instance. At the heart of Supabase is a Postgres instance and Prisma can be used to connect to it and run queries with ease. Run npm i -D prisma to add it to our project and then run npx prisma init to initialize Prisma in our NestJS project. Additionally, we will also need the Prisma client to use Prisma in our project. So go ahead and run npm i @prisma/client.

Note how prisma is a dev dependency but @prisma/client is not a dev dependency.

Difference between DATABASE_URL and DIRECT_URL in Supabase

Supabase

DIRECT_URL (1.) allows us to directly connect to the Supabase instance without our connection being pooled. It is like an admin link to connect to the underlying Postgres DB. It is good for migrations and introspections. It connects directly to Port 5432.

But DATABASE_URL (2.) is the more general link to connect to DB to run queries. The connections through this link are pooled. These are good for in-code usage. This does not connect to port 5432 but a mapped port and link which can be different for every Supabase user.

Creating a Prisma Module in NestJS

There is an NPM package called nestjs-prisma which can be used to provide connectivity between a NestJS and a DB using Prisma within a matter of seconds. It also comes with a Global Exception Filter to handle Prisma query-related errors. But I find creating a custom module to be a better way. So that’s what we are going to do here.

We don’t need controllers since the PrismaModule we are about to create won’t handle any endpoints. We just need a module and a service to encapsulate the business logic. So, we need to run two separate Nest CLI commands successively - nest g module prisma and nest g service prisma –no-spec. This will create a folder called prisma inside our src folder.

We just to slightly edit the prisma.module.ts file by adding the @Global() decorator to the PrismaModule we just created as shown below. Along with this, we will also add PrismaService to the list of exports. This will allow us to use the service in any module without importing it.

import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService]
})
export class PrismaModule {}

For our PrismaService, we need it to inherit from PrismaClient (imported from @prisma/client) and then connect lazily to the DB. This can be done using the code below. Between lines 8 and 14, we provide the Database URL to Prisma for connection. This URL is available in the Supabase settings under Connection pooling custom configuration subsection in the Database tab (add the connection string with the password to our .env file). The code below shows our prisma.service.ts file.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient {
  constructor(configService: ConfigService) {
    super({
      datasources: {
        db: {
          url: configService.get('DATABASE_URL'),
        },
      },
    });
  }

}

Schema Introspection with Prisma

We won’t be defining the Schema. We won’t be copy-pasting it either. We will be using this cool feature called DB Introspection. Prisma will connect to the Postgres DB inside Supabase and automatically fill the schema.prisma file with the Schema of the tables inside the DB instance.

But to connect to the DB instance and introspect, we will not be using the DATABASE_URL. We will be using the DIRECT_URL. This can also be found in the Supabase settings in the Database tab under the Connection String sub-section. Add this to our .env file and then inside the schema.prisma file at the base of our backend project, add it to the datasource. Our schema.prisma file will look like this.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

After this, we need to run npx prisma db pull and leave the rest to Prisma. When this command completes, our schema.prisma file will be populated with the schema of the tables already in our Supabase instance.

So now we have our schema ready and our project connected to the Supabase instance we created in the last article.

Authentication Endpoints (AuthModule)

After all of these steps, we will now move towards amalgamating what we have done in this sub-section. That starts with the creation of the AuthModule. In NestJs, you can easily create a set of controller endpoints, abstract the logic to a service, and create a complete module to use these through just a single line of code. Run nest g resource auth –no-spec to create the AuthModule. Keep in mind that our AuthModule will consist of REST API endpoints only and we only need 2 custom controllers which we will define ourselves below so no need to create the CRUD endpoints through this CLI.

On completion of the above command, an AuthModule will be created and imported into our main AppModule. Since we used the --no-spec file with the Nest CLI, no test files will be created. We will a Data Transfer Object or DTO to hold the Google User profile information. For this reason, create a dto folder inside the auth folder if you don’t have any, and then create a file called google-login.dto.ts. This file will contain our Google Profile info as given below.

export class GoogleLoginUserDto {
  providerAccountId: string;
  email: string;
  email_verified: string;
  firstName: string;
  lastName: string;
  picture: string;
  accessToken: string;
  refreshToken: string;
  id_token: string;
  expires_in: number;
}

Next, move to the auth.controller.ts file and replace the contents of the file with the code below. In the code below, we define two endpoint routes at lines 15 and 18. Both of them will handle GET requests so they are annotated with the @Get() decorator. The AuthController will serve the base endpoint /auth. In line 9, we tell NestJS to set google-login metadata key as true for every request directed towards the AuthModule controller. This will allow the requests to by-pass the global JWT guard that we will set in a few moments.

At line 10, we are telling NestJS that any requests to this Controller are supposed to first pass through HttpGoogleOAuthGuard that we defined in the above sub-section. This can be observed from the @Controller(‘auth’) decorator at line 11 below.

import { Controller, Get, Req, SetMetadata, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Request } from 'express';
import { HttpUser } from 'src/decorators';
import { HttpGoogleOAuthGuard } from 'src/guards';
import { GoogleLoginUserDto } from './dto/google-login.dto';


@SetMetadata('google-login', true)
@UseGuards(HttpGoogleOAuthGuard)
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get()
  async googleAuth(@Req() _req: Request) {}

  @Get('google-redirect')
  googleAuthRedirect(@HttpUser() user: GoogleLoginUserDto) {
    return this.authService.googleLogin(user);
  }
}

Through the above code, we define the authN flow. Any login requests will come to /auth (GET) endpoint and be handled by the controller at line 15. Without any effort of ours, the user will be redirected to the Google Login page. After their login, they will be re-directed from Google to /auth/google-redirect endpoint which will be handled by the controller at line 18. This controller will receive the Google User profile data and will load that into the GoogleLoginUserDto for us with the help of @HttpUser() decorator which we defined before. We pass this user data to the googleLogin() method of the AuthService inside auth.service.ts file. The contents of the auth.service.ts are shown below.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from 'src/prisma/prisma.service';
import { GoogleLoginUserDto } from './dto/google-login.dto';

@Injectable()
export class AuthService {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
    private prisma: PrismaService,
  ) {}
  async googleLogin(user: GoogleLoginUserDto) {
    if (!user) {
      throw new UnauthorizedException('No user from google');
    }
    const {
      firstName,
      lastName,
      email,
      email_verified,
      expires_in,
      picture,
      providerAccountId,
      accessToken,
      refreshToken,
      id_token,
    } = user;
    const userData = await this.prisma.users.findFirst({
      where: { email},
      include: { accounts: true },
    });
    if (!userData) {
      const newUserData = await this.prisma.users.create({
        data: {
          name: `${firstName} ${lastName}`,
          email: email,
          emailVerified: email_verified? (new Date()).toISOString(): null,
          image: picture,
          accounts: {
            create: {
              type: 'oauth',
              provider: 'google',
              providerAccountId: providerAccountId,
              access_token: accessToken,
              refresh_token: refreshToken,
              id_token: id_token,
              expires_at: expires_in,
            },
          },
        },
      });
      const access_token = await this.signJwt(
        newUserData.id,
        id_token,
        accessToken,
        expires_in,
      );
      return { access_token };
    }
    const access_token = await this.signJwt(
      userData.id,
      id_token,
      accessToken,
      expires_in,
    );
    return { access_token };
  }
  signJwt(
    userId: string,
    id_token: string,
    access_token: string,
    expires_at: number,
    expiresIn = '1d',
  ): Promise<string> {
    const payload = {
      sub: userId,
      id_token,
      access_token,
      expires_at,
    };
    return this.jwtService.signAsync(payload, {
      expiresIn,
      secret: this.configService.get('APP_JWT_SECRET'),
    });
  }
}

In the service above, we will need:

  1. JwtService for minting a new auth_token (used for authZ) to the user.

  2. PrismaService to query the Supabase DB to find out if it’s a new user or not.

  3. ConfigService to fetch the JWT secret (APP_JWT_SECRET env variables).

We define these in the constructor of the AuthService class. These will be injected by the NestJS runtime. We use the private shorthand to make it class-scoped. Inside the googleLogin() function, we check if Google has passed us a valid user or not at line 15. If not, then terminate the request.

If a valid user profile has been sent from Google, then at line 30 we query the Supabase DB’s User table to find if we have a user with the same email as sent by Google. If we don’t then it's safe to assume that it’s a new user. So, with the check at line 34, we insert the user into our User Table and create an entry in the Account table. We then create our auth_token called access_token and send it back as a response. If the user already exists, then we simply mint an authZ JWT and send it back regardless between lines 62 and 68.

Between lines 70 and 87, we define a wrapper for JWT minting. This wrapper, called signJwt(), takes the user ID from our User table, the opaque access_token, id_token & expires_at passed by Google and uses these as a payload to create a new JWT.

Importing Strategies and Attaching Guards

Now before we finish off the AuthModule, there are a couple of things still pending. We need to add the JWT Auth Guard we created Globally. We also need to import the Google OAuth Strategy and JWT Strategy in our AppModule so that it is recognized app-wide.

For registering our Auth guard globally, we can just register it locally in any of the modules as a provider. Since we have created an AuthModule, that’s where we are going to add it. Our AuthModule would, thus, look something like this:

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from 'src/guards';

@Module({
  controllers: [AuthController],
  providers: [
    AuthService,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AuthModule {}

Finally, we can import our strategies into our main app module. With all the changes we have done above, our app module with the strategies would come to look something like the code given below:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import * as Joi from 'joi';
import { GoogleStrategy } from './strategies';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './strategies/jwt.strategy';
import { PrismaModule } from './prisma/prisma.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DATABASE_URL: Joi.string().required(),
        DIRECT_URL: Joi.string().required(),
        GOOGLE_CLIENT_ID: Joi.string().required(),
        GOOGLE_CLIENT_SECRET: Joi.string().required(),
        GOOGLE_CALLBACK_URL: Joi.string().required(),
        APP_JWT_SECRET: Joi.string().required()
      }),
    }),
    AuthModule,
    PassportModule,
    JwtModule.register({
      global: true,
    }),
    PrismaModule,
  ],
  controllers: [AppController],
  providers: [AppService, GoogleStrategy, JwtStrategy],
})
export class AppModule {}

This completes our addition of Authentication and Authorization to our backend. It should be able to access the same Database as our frontend NextJS app does with Next Auth. This would mean all our user data is in one place and a user can independently login from the frontend and backend. This opens up avenues like using the backend for Mobile App (something that we won’t cover).

Testing out Backend

The coding part of creating an authentication and authorization flow is not complete. We will now begin testing our NestJS application to make sure everything works as we expect.

In the video demo below, we run npm run dev to start the Turbo dev server. Our NestJS backend starts at the port 3001 of localhost. As per the design, we have 3 routes. Since the JWT Auth guard is being used globally, all route requests will pass through it unless the route controller is annotated with @Public() decorator. If you try to access http://localhost:3001/, it will give you an Unauthorized Message as shown in the video below.

We first need to head over to http://localhost:3001/auth. From there, we will be redirected to Google Login as shown above. After Google login, we are redirected to http://localhost:3001/auth/google-redirect. This route controller takes in the Google OAuth profile sent back to our app and mints a JWT token. The access_token sent in our video is in fact the NextJS frontend app’s auth_token counterpart.

When we attach this as the bearer token in auth header, we see that we are shown the ”Hello World!” message on http://localhost:3001. This means our backend is working just as expected. You can try to head over to Supabase to verify if the user is created or not.

Frontend & Backend Interaction in a NextJS and NestJS app

Image depicting NextJS interacting with NestJS

The image above depicts the interaction diagram of our mini-project thus far. The Users come with their Authentication request and then

  1. On the frontend, NextJS + Next-Auth handles it using the GoogleProvider.

  2. On the backend, NestJS + Passport handles it using the GoogleStrategy. This provides two independent but co-operating avenues for AuthN.

Making the Next App Work with Nest Authentication

Now to make these interact, we will make a slight change to our SignoutButton component in the _components directory of our frontend NextJS project. We will make it so that the SignoutButton component logs the auth_token if the user is authenticated. We will take this auth_token JWT and use it as a bearer to access http://localhost:3001, i.e., the protected Nestjs backend route. If we receive a successful response then we can be sure that the auth_token is fulfilling its purpose as AuthZ token. Our SignoutButton component will look like this:

"use client";
import { signIn, signOut, useSession } from "next-auth/react";
import React from "react";

const SignoutButton = () => {
  const { data: session } = useSession();
  console.log("Session data", session)
  if (session?.user) {
    return (
      <div className="flex gap-4 ml-auto">
        <p className="text-sm tracking-tight text-sky-400">
          {session.user.name}
        </p>
        <button onClick={() => signOut()}>Sign Out</button>
      </div>
    );
  }
  return <button onClick={() => signIn()}>Sign in</button>;
};

export default SignoutButton;

While at it, we will also use a new Google User to sign in from our front end. This will make sure that Supabase creates a new record which will then be accessed independently from our backend as shown in the video demo below:

Conclusion

This concludes our long blog post. We finish what we had started in the first article of this series. With this, we have completed the Authentication and Authorization flow. We now have a NextJS app utilizing Next Auth and Google Provider which can interact with a NestJS backend app.

From the next article onwards, we will start building the core of our project. We will start with the backend and then move to the frontend. Until then keep building awesome things!

Did you find this article valuable?

Support Bored on the Edge by becoming a sponsor. Any amount is appreciated!