Implementing Google OAuth Authentication in Next.js Using Passport.js and Serverless Functions
Tags: nextjs, oauth, webdev, google
Published: 2023-08-26 16:00:07

User authentication is a critical component of web applications, ensuring security and personalized experiences for users. Google OAuth authentication is a popular choice, allowing users to sign in using their Google accounts. In this tutorial, we’ll explore how to implement Google OAuth authentication in a Next.js application using [Passport.js](https://www.passportjs.org/), [passport-google-oauth](https://www.passportjs.org/packages/passport-google-oauth2/), [next-connect](https://www.npmjs.com/package/next-connect), and serverless functions.

## What is OAuth?
[OAuth](https://en.wikipedia.org/wiki/OAuth), in essence, is a delegated authorization framework. It enables third-party applications to access specific user data hosted by another service provider, such as Google, without requiring the user to reveal their credentials. Instead of sharing passwords, OAuth grants access using access tokens and refresh tokens.

### Benefits of OAuth for Authentication
OAuth offers several advantages for implementing user authentication:

* **Security**: Users’ credentials remain confidential, reducing the risk of data breaches due to compromised passwords.
* **Simplified User Experience**: OAuth simplifies the user experience by allowing them to grant selective access without sharing their complete account information.
* **Authorization Granularity**: Service providers can define scopes that control the level of access granted to third-party applications.
* **No Password Exposure**: Third-party applications don’t receive or store user passwords, increasing security.
Single Sign-On (SSO): OAuth supports single sign-on, allowing users to access multiple applications with a single set of credentials.

### Google OAuth Integration
One popular use case of OAuth is integrating with Google’s authentication services. Google OAuth enables users to log into applications using their Google accounts, benefiting from Google’s robust security measures and convenient login process.

Integrating Google OAuth with a Next.js application involves configuring your app as a trusted OAuth client within the Google Developer Console. This enables your app to request access to user data, such as their Google profile information, email, or other specified permissions.

In the next sections, we’ll delve into the technical steps required to set up Google OAuth authentication in a Next.js application using serverless functions. This approach combines the security of Google’s authentication services with the efficiency and flexibility of serverless functions to create a seamless authentication flow for your users.

## Serverless Functions in Next.js
In recent years, serverless architecture has gained significant traction in web development due to its scalability, cost-effectiveness, and ease of deployment. Serverless functions, also known as serverless APIs or lambda functions, allow developers to execute code in response to events without managing server infrastructure.

### Introduction to Serverless Functions
Serverless functions are self-contained pieces of code that perform specific tasks and are executed on-demand. They are designed to be stateless, meaning they don’t store data between invocations. Instead, they handle a single request and provide a response. This architecture allows developers to focus on writing code without the overhead of managing server infrastructure.

### Why Use Serverless Functions?
Serverless functions offer several advantages that make them appealing for various use cases:

* **Scalability**: Serverless functions automatically scale based on demand, ensuring optimal performance during traffic spikes.
* **Cost Efficiency**: You pay only for the resources consumed during function execution, reducing infrastructure costs.
Simplified Deployment: Deploying serverless functions is straightforward, and providers handle deployment details.
* **Faster Development**: Serverless functions encourage modular development and rapid iteration.
* **Managed Infrastructure**: Cloud providers manage server provisioning, scaling, and maintenance.

### How Serverless Functions Work in Next.js
[Next.js](https://nextjs.org/), a popular [React framework](https://react.dev/), offers built-in support for [serverless functions](https://vercel.com/docs/functions/serverless-functions). These functions can be created directly within your Next.js project and seamlessly integrated with your application. With serverless functions in Next.js, you can build backend APIs, handle form submissions, and perform various tasks without the need for a dedicated server.

In a Next.js project, serverless functions are placed in the `pages/api` directory. Each file in this directory represents a serverless function that can be invoked via HTTP requests. The serverless functions are automatically deployed along with your Next.js application when you deploy it to platforms like [Vercel](https://vercel.com/dashboard), [Netlify](https://www.netlify.com/), or other serverless hosting providers.

In the next section, we’ll dive into the process of setting up Google OAuth authentication in a Next.js application using serverless functions. We’ll combine the power of OAuth with the flexibility of serverless architecture to create a secure and efficient authentication flow for your users.

## Prerequisites
Before you begin, make sure you have the following set up:

* Node.js and npm installed
* A Google Developer Console project with OAuth 2.0 credentials

## Steps
### 1. Set Up Google OAuth Credentials
* Go to the [Google Developer Console](https://console.developers.google.com/).
* Create a new project or use an existing one.
* Navigate to “APIs & Services” > “Credentials.”
* Create credentials and select “OAuth 2.0 Client IDs.”
Configure your authorized redirect URIs. In our example: `http://localhost:3000/api/account/google/callback`

### 2. Install Dependencies
In your Next.js project, install the necessary dependencies:
```shell
npm install passport passport-google-oauth next-connect
```

### 3. Implementing the OAuth Login Flow
1. **Create a Login Button**: In your application’s UI, create a button that users can click to initiate the OAuth login flow.
2. **Redirect to OAuth Endpoint**: When the user clicks the login button, redirect them to the OAuth authorization URL you constructed using your OAuth Client ID and the appropriate scope.
3. **OAuth Callback Handling**: After the user grants access and Google redirects back to your application, your serverless function should handle the OAuth callback. Retrieve the authorization code from the query parameters and exchange it for an access token.
4. **Store User Data**: Once you receive the access token, you can use it to request user data from Google. Store relevant user information in your application’s session or database.

By following these steps, you can implement Google OAuth authentication in your Next.js application without using third-party libraries like `next-auth`. This approach provides you with full control over the authentication process and allows you to tailor the implementation to your specific needs. While it requires more manual configuration, it offers a deeper understanding of how OAuth works and allows for more customization.

### 4. Create OAuth Strategy and Callback Serverless Function
Create an OAuth strategy using `passport-google-oauth`. Create a file named `pages/api/account/google/[operation].ts`:

```typescript
import type { NextApiRequest, NextApiResponse } from "next";
import { createRouter } from "next-connect";
import passport from "passport";
import Google, {
  IOAuth2StrategyOption,
  Profile,
  VerifyFunction,
} from "passport-google-oauth";
import { findUser, sendWelcomeEmail } from "@/app/account/actions";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { createUserFromProvider } from "../lib";
import { get } from "lodash";
import { getSecureCookie } from "@/app/lib/services/cookies";

dayjs.extend(duration);

export const config = {
  api: {
    externalResolver: true,
  },
};

passport.use(
  new Google.OAuth2Strategy(
    {
      clientID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: process.env.NEXT_PUBLIC_GOOGLE_CALLBACK_URL,
    } as IOAuth2StrategyOption,
    (
      accessToken: string,
      refreshToken: string,
      profile: Profile,
      done: VerifyFunction
    ) => {
      done(null, profile);
    }
  )
);

const router = createRouter<NextApiRequest, NextApiResponse>();

router.get((req, res, next) => {
  if (req.query.operation === "auth") {
    return passport.authenticate("google", { scope: ["profile", "email"] })(
      req,
      res,
      next
    );
  } else if (req.query.operation === "callback") {
    return passport.authenticate(
      "google",
      async (err: any, profile: any, info: any) => {
        if (err) {
          res.status(500).end();
          return;
        }
        let user = await findUser({
          email: profile.emails[0].value,
        });
        if (!user) {
          user = await createUserFromProvider({
            profile,
            identityProvider: "GOOGLE",
          });
          setTimeout(async () => {
            await sendWelcomeEmail({ email: user!.email });
          }, 0);
        } else if (user.identityProvider !== "GOOGLE") {
          res.redirect("/?error=exists_with_different_identity");
          return;
        }
        const cookie = await getSecureCookie({
          name: "user",
          value: {
            email: user!.email,
            id: user!.id,
            photo: get(profile, "photos[0].value", ""),
            name: get(profile, "displayName", ""),
            username: user!.username,
          },
        });
        res.setHeader("Set-Cookie", cookie);
        res.redirect("/profile");
      }
    )(req, res, next);
  } else {
    res.status(400).json({ error: "Unknown operation." });
  }
});

export default router.handler({
  onError: (err: any, req: any, res: any) => {
    console.error(err.stack);
    res.status(err.statusCode || 500).end(err.message);
  },
});
```

The provided code is an implementation of Google OAuth authentication using Passport.js, Next.js API routes, and some custom logic for user management. Let’s break down the code step by step:

1. **Import Statements**: The code starts by importing various modules and functions required for the authentication process.
2. **Passport Configuration**: The `passport.use` function configures the Google OAuth2Strategy with the provided client ID, client secret, and callback URL. This strategy handles the authentication process with Google.
3. **Router Setup**: The `createRouter` function from `next-connect` is used to create an API route handler (`router`). The router will handle different operations based on the query parameter named `operation`.
4. **Google OAuth Flow**: If the `operation` is set to `"auth"`, it triggers the Google OAuth authentication process. The user will be redirected to Google's login page to authenticate their account. If the `operation` is set to `"callback"`, it handles the callback after the user logs in with their Google account. The `passport.authenticate` function is used to authenticate the user based on the Google OAuth strategy. If the authentication is successful (`profile` contains user information), it checks if the user already exists in the local database based on their email. If the user does not exist, a new user is created using the `createUserFromProvider` function. A welcome email is also sent to the new user asynchronously. If the user exists but has a different identity provider (not Google), the user is redirected with an error message. If the user exists and has Google as their identity provider, a secure cookie containing user information is generated and set in the response header. The user is then redirected to the profile page. Finally, if there is an error during authentication, a 500 status code is returned.
5. **Error Handling**: The `onError` handler is defined to handle any errors that might occur during the authentication process. If an error occurs, it logs the error and sends an appropriate response.
6. **Export and Handling**: The `router.handler` function is used to export the API route handler. This handler will be invoked when a request is made to the specified API route.

In summary, the code follows the Google OAuth authentication flow using Passport.js, fetches user information, checks for existing users, and handles the creation of new users or redirects based on the user’s identity. It also takes care of error handling and response generation throughout the process.

## Securing and Managing User Sessions
User sessions play a crucial role in maintaining a seamless and secure authentication experience in your Next.js application. By properly securing and managing user sessions, you can ensure that users stay authenticated during their interactions with your platform.

### Storing and Managing User Sessions
1. **Session Tokens or Cookies**: Choose between using session tokens or HTTP cookies to manage user sessions. Session tokens are often stored in memory or databases, while cookies are stored in the user’s browser.
2. **Token-Based Approach**: If you opt for a token-based approach, generate a unique session token for each authenticated user. This token can be stored in the browser’s `localStorage` or `sessionStorage` and sent with each request to identify the user.
3. **Cookie-Based Approach**: With a cookie-based approach, set an HTTP cookie containing the user’s session ID. Cookies are automatically included in subsequent requests, allowing your serverless functions to identify the user.

### Using Cookies or Tokens for Session Management
1. **Cookies**: Cookies provide built-in security features, such as the [SameSite](https://web.dev/samesite-cookies-explained/) attribute, which helps prevent cross-site request forgery (CSRF) attacks. However, cookies have size limitations, and users can disable them.
2. **Tokens**: Using tokens allows you to implement additional security measures, such as token expiration and refresh tokens. However, token-based approaches require additional development to handle token expiration and renewal.

## Enhancing User Experience
A seamless user experience is essential in any authentication flow. Enhancements such as handling loading states, managing errors, and personalizing user profiles can greatly impact how users perceive your application’s authentication process. In this section, we’ll explore ways to enhance the user experience during and after the Google OAuth authentication process.

### Handling Loading States and Errors
1. **Loading States**: During the authentication flow, users may experience brief waiting periods. Implement loading indicators to inform users that their request is being processed.
2. **Error Handling**: Anticipate and handle errors gracefully. Display user-friendly error messages for scenarios like invalid credentials, network issues, or server errors.

### Redirecting After Successful Authentication
1. **Redirecting to the Home Page**: After a user successfully authenticates via Google OAuth, redirect them to the appropriate page in your application. Typically, this is the home page or a dashboard.
2. **Personalized Greetings**: Consider displaying a personalized greeting message on the redirected page to enhance the user experience. Welcome the user by their name, retrieved from the user data fetched during the OAuth process.

### Personalizing User Profiles
1. **Fetching User Data**: After authentication, retrieve additional user data using the access token. Fetching data such as user profile images, names, and email addresses can help you create a more personalized experience.
2. **Displaying User Data**: Display fetched user data on the user’s profile page or within the application interface. Personalized content reinforces the value of authentication and demonstrates your application’s user-centric approach.

### User Preferences and Customization
1. **Allowing User Preferences**: Give users the option to customize their experience by providing settings or preferences. For example, users might choose their preferred language or theme.
2. **Consent Management**: If your application requires additional permissions beyond basic user information, clearly explain the purpose of each permission and allow users to grant or revoke consent.

Enhancing the user experience during Google OAuth authentication involves thoughtful design and attention to detail. By addressing loading states, errors, and personalization, you create a user-friendly atmosphere that encourages engagement and fosters trust.

## Verification Process for Production
When you’re ready to take your application live and allow users to authenticate using Google OAuth, you must go through Google’s verification process. This process involves submitting your application for review by Google to ensure it complies with their policies and security standards. Verification is essential to prevent unauthorized use of user data and ensure a secure experience for your users.

![Unverified application screen.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9aasnpdenqx6gb6yqoq3.png)

Once your application successfully passes the verification process, you’ll receive the green light to use Google OAuth in production. This step is crucial to provide a secure and reliable authentication experience to your users.

As you navigate the verification process and transition your application to a production environment, the benefits of Google OAuth and serverless functions will continue to provide a solid foundation for secure user authentication. Remember that the journey doesn’t end here; continuous monitoring, updates, and enhancements will ensure a seamless and trustworthy experience for your application’s users.

## Conclusion
Implementing Google OAuth authentication in your Next.js application using serverless functions is a powerful way to create a secure and user-friendly authentication process. In this guide, we’ve covered the entire journey, from understanding OAuth authentication to deploying your application with enhanced security and user experience. Let’s recap the benefits and next steps:

Recap of the Process

1. **Understanding OAuth**: You’ve gained a deep understanding of OAuth and its advantages for secure user authentication without exposing credentials.
2. **Serverless Functions in Next.js**: You’ve explored how serverless functions work in Next.js and why they are an excellent choice for implementing authentication.
3. **Setting Up Google OAuth**: You’ve learned how to create a Google Developer Project, configure OAuth credentials, and integrate OAuth with your Next.js application.
4. **Creating Serverless Functions**: You’ve created serverless functions to handle OAuth callbacks, exchange authorization codes for tokens, and fetch user data.
5. **Securing User Sessions**: You’ve understood the importance of secure session management and implemented measures to ensure user privacy and authorized access.
6. **Enhancing User Experience**: You’ve discovered how to handle loading states, errors, and personalize user profiles, creating a smooth and engaging authentication flow.

By following the steps outlined in this guide, you’ve successfully implemented Google OAuth authentication in your Next.js application using serverless functions. This accomplishment not only enhances security but also contributes to a user-centric experience that fosters trust and engagement. As you continue to refine and expand your application, you’ll create a robust platform that users can rely on for secure and seamless authentication.

Happy coding!