DEV Community

Cover image for How to Implement Google OAuth 2.0 in Next.js with NestJS
Marwan Zaky
Marwan Zaky

Posted on • Edited on

How to Implement Google OAuth 2.0 in Next.js with NestJS

Step by step on how to implement Google OAuth 2.0 in Next.js with NestJS.

Demo example: https://mamolio.vercel.app/signin

Demo code: https://github.com/marwanzaky/mern-ecommerce

Sign in with Google demo

1. Create Google OAuth Credentials

  1. Go to: http://console.cloud.google.com
  2. Create a project
  3. Go to: APIs & Services > Credentials > Create credentials > OAuth client ID
  4. Choose:

App type: Web application

Add:

Authorized redirect URI

http://localhost:3001/auth/google/callback

Credentials

  1. Click Create and copy CLIENT_ID, and CLIENT_SECRET

2. Backend (NestJS)

Install Dependencies

npm install passport @types/passport @nestjs/passport passport-google-oauth20 @types/passport-google-oauth20
Enter fullscreen mode Exit fullscreen mode

Create Google Strategy

// google.strategy.ts
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy, VerifyCallback } from "passport-google-oauth20";

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
    constructor() {
        super({
            clientID: process.env.GOOGLE_CLIENT_ID!,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
            callbackURL: `${process.env.SERVER_URL!}/auth/google/callback`,
            scope: ["email", "profile"],
        });
    }

    async validate(
        accessToken: string,
        refreshToken: string,
        profile: {
            name: { familyName: string; givenName: string };
            emails: { value: string }[];
        },
        done: VerifyCallback,
    ) {
        const { emails, name } = profile;

        const user = {
            email: emails[0].value,
            firstName: name.givenName,
            lastName: name.familyName,
        };

        done(null, user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Auth Controller

// auth/auth.controller.ts
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth() {
    // redirects to Google
}

@Get("google/callback")
@UseGuards(AuthGuard("google"))
async googleAuthRedirect(@Req() req: any, @Res() res: any) {
    const response = await this.authService.loginWithGoogle(req.user);
    return res.redirect(`${process.env.CLIENT_URL!}/auth/success?token=${response.token}`);
}
Enter fullscreen mode Exit fullscreen mode

Auth Service

// auth/auth.service.ts
async loginWithGoogle(user: {
    email: string;
    firstName: string;
    lastName: string;
}): Promise<{ token: string }> {
    const existingUser = await this.usersService
        .findByEmail(user.email);

    if (!existingUser) {
        const randomPassword = generatePassword();

        const newUser = await this.usersService.create({
            email: user.email,
            name: `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim(),
            password: randomPassword,
        });

        return {
            token: await this.createAccessToken(newUser.id, newUser.role)
        };
    }

    return {
        token: await this.createAccessToken(existingUser.id, existingUser.role),
    };
}

async createAccessToken(userId: string, role: UserRole) {
    return await this.jwtService.sign(
        { id: userId, role },
        {
            secret: process.env.JWT_SECRET!,
            expiresIn: process.env.JWT_EXPIRES!,
        },
    );
}
Enter fullscreen mode Exit fullscreen mode

App Module

// app.module.ts
@Module({
    imports: [
        ConfigModule.forRoot({
            envFilePath: ".env",
            isGlobal: true,
        }),

        MongooseModule.forRootAsync({
            imports: [ConfigModule],
            inject: [ConfigService],
            useFactory: async (config: ConfigService) => ({
                uri: config.get<string>("MONGODB_URI"),
            }),
        }),

        AuthModule,
        UsersModule,
    ],
    providers: [
        JwtService,
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
        {
            provide: APP_GUARD,
            useClass: RolesGuard,
        },
    ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Environment Variables

CLIENT_URL="http://localhost:3000"
SERVER_URL="http://localhost:3001"
GOOGLE_CLIENT_ID="your_google_client_id"
GOOGLE_CLIENT_SECRET="your_google_client_secret"
JWT_SECRET="your_jwt-secret"
JWT_EXPIRES=30d
Enter fullscreen mode Exit fullscreen mode

3. Frontend (Next.js)

Signin Page

// app/signin/page.tsx
const handleGoogleLogin = () => {
    window.location.href = `${process.env.NEXT_PUBLIC_SERVER!}/auth/google`;
};

<button onClick={handleGoogleLogin}>Continue with Google</button>;
Enter fullscreen mode Exit fullscreen mode

Success Page

// app/success/page.tsx
"use client";

import { useRouter, useSearchParams } from "next/navigation";

export default function Page() {
    const router = useRouter();
    const searchParams = useSearchParams();

    useEffect(() => {
        const token = searchParams.get("token");

        if (token) {
            localStorage.setItem("token", token);
            router.push("/");
        }
    }, [searchParams, router]);

    return <p>Logging you in...</p>;
}
Enter fullscreen mode Exit fullscreen mode

Environment Variables

NEXT_PUBLIC_SERVER="http://localhost:3001"
Enter fullscreen mode Exit fullscreen mode

3. Flow (What Happens)

  1. User clicks “Continue with Google”
  2. Redirect → Google login page
  3. Google redirects back → /auth/google/callback
  4. NestJS:
    • extracts profile
    • creates/fetches user
    • returns JWT
  5. You store JWT in frontend

You are all set to use the Sign in with Google button for the web. You can review my code to implement “Sign In with Google” for Next.js and NestJS on GitHub.

Top comments (0)