Building a t3-app : A Step-by-Step Guide to NextAuth
Tags: tutorial, nextjs, webdev, react
Published: 2023-03-08 00:26:48

NextAuth not only does handle most of your client and server-side logic with ease, but you can also create multiple authentication providers. You will understand how NextAuth works and how to use the `credentials provider` to create a JWT accessToken and refresh it. So, let's dive in and use [t3-app](https://create.t3.gg/) and MongoDB to build a full-stack authentication application with NextAuth. 


> 1) Create a t3 app project

use npm:
```
npm create t3-app@latest
```
or yarn:
```
yarn create t3-app@latest
```

> 2) Setup a MongoDB in Prisma


To make Prisma work in MongoDB , we need to get a Replica Set on [MongoDB Atlas](https://www.mongodb.com/cloud/atlas/signup)

Edit .env file in root directory, and change `DATABASE_URL`: 
```
DATABASE_URL="mongodb+srv://YOUR_DB_URL"
```
Make sure to replace special characters to [ASCII value](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) e.g. `"?"` replace to `"%3F"`

To store user data, we need to create a User schema. We'll need to replace the current schema located at ./prisma/schema.prisma with the following:

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

datasource db {
    provider = "mongodb"
    url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid()) @map("_id")
  name          String
  password      String
}

```

Then run `npx prisma db push` command to sync our changes with MongoDB.


> 3) Setup a NextAuth
   
 
If you're new to JWT authentication, you can watch this video for a more detailed explanation of the process:

{% youtube iD49_NIQ-R4 %}


In NextAuth, we can choice [JWT strategy](https://authjs.dev/guides/basics/refresh-token-rotation#jwt-strategy) or [Database strategy](https://authjs.dev/guides/basics/refresh-token-rotation#database-strategy). In this demo, we will use `JWT strategy` for setup NextAuth.


Add `JWT_SECRET` and `NEXTAUTH_SECRET` in `.env` file. The secret is a hash key to sign and verify JWT.

```javascript
... 
  DATABASE_URL="mongodb+srv://your db url"
+ JWT_SECRET="your secret"
+ NEXTAUTH_SECRET="your secret"
```

If you're using a Mac OS or Linux system, you can generate secret using the following command:

```
openssl rand -base64 32
```

Next, we need to install bcrypt in order to hash the user's password:

use npm:
```
npm install bcrypt -D @types/bcrypt
```
or yarn:
```
yarn add bcrypt --dev @types/bcrypt
```

Let's take a closer look at the main steps involved in how NextAuth works within our app:

1. [Providers](https://next-auth.js.org/configuration/providers/credentials): When a user initiates the authentication process, it will send authentication providers and their configuration options to the client-side. We will use `credentials` provider `Username` and `Password` for user login/signup. In addition, you can add addition [provider](https://next-auth.js.org/providers/) to your nextAuth.If the authentication process is successful, the function will return {user:{id: string, name: string}}. If authentication fails, it will return null. In either case, the data is passed to the JWT callback function as `{user}`


2. [Callback - JWT](https://authjs.dev/guides/basics/callbacks#jwt-callback): This function is called whenever a request is made to `/api/auth/signin`, as well as after the `signIn` callback function has been executed. And pass `{token}` to session callback function. After the user has successfully logged in, we can retrieve their data from the params `{user}`. If the user exists, we can then proceed to create both the `accessToken` and `refreshToken`. Subsequently, the callback function is also called during requests made to `getSession()`, `unstable_getServerSession()`, and `useSession()`. During each of these requests, the callback function is the first to check whether the JWT access token has expired. If it has, the JWT callback will attempt to refresh the token using the refresh token. For a more detailed look at how to implement this process： [refresh token rotation](https://authjs.dev/guides/basics/refresh-token-rotation).


3. [Callback - session](https://authjs.dev/guides/basics/callbacks#session-callback): The session callback will return session object that can be used to display the user's name or any other user data that's relevant to the application. The callback function will be called when  `getSession()`, `useSession()`, `/api/auth/session`


To create and verify JWT tokens, we need to create a function within the `src/utils/` directory called `jwtHelper.ts`. This function will handle the creation and verification of the JWT tokens that are used throughout our app. 

```javascript

import { encode, decode } from "next-auth/jwt";
import { env } from "@/env.mjs";
import { User } from "@prisma/client";

export interface AuthUser extends Omit<User, "Password">{}

export const tokenOneDay = 24 * 60 * 60;
export const tokenOnWeek = tokenOneDay * 7 

const craeteJWT = (token:AuthUser, duration: number) => encode({token, secret: env.JWT_SECRET, maxAge: duration})

export const jwtHelper = {
  createAcessToken: (token:AuthUser) => craeteJWT(token, tokenOneDay),
  createRefreshToken: (token:AuthUser) => craeteJWT(token, tokenOnWeek),
  verifyToken: (token:string) => decode({token, secret: env.JWT_SECRET})
}
```

We need to define the NextAuthType type in order to integrate it properly within our app. To do so, we'll create a new file called `next-auth.d.ts` within the `src/pages/types/ directory`. For more information on how to [extend default] (https://next-auth.js.org/getting-started/typescript#extend-default-interface-properties) interface properties and properly define this type.

```javascript
import NextAuth from "next-auth/next";
import { JWT } from "next-auth/jwt"
import { AuthUser } from "@/utils/jwtHelper";

declare module "next-auth" {
  interface User {
    userId?: string,
    name?: string
  }

  interface Session {
    user: {
      userId?: string,
      name?: string 
    },
    error?: "RefreshAccessTokenError"
  }
}

declare module "next-auth/jwt" {
  interface JWT{
    user: AuthUser
    accessToken: string,
    refreshToken: string,
    accessTokenExpired: number,
    refreshTokenExpired: number,
    error?: "RefreshAccessTokenError"
  }
}

```

Change `src/pages/api/auth/[...nextauth].ts` as follow: 
```javascript

import { type GetServerSidePropsContext } from "next";
import {
  getServerSession,
  type NextAuthOptions,
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/server/db";
import bcrypt from 'bcrypt';
import { AuthUser, jwtHelper, tokenOneDay, tokenOnWeek } from "@/utils/jwtHelper";

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: "jwt",
    maxAge: 60 * 60
  },
  providers: [
    CredentialsProvider({
      id: "next-auth",
      name: "Login with email",
      async authorize(credentials, req) {
        try {
          const user = await prisma.user.findFirst({
            where: {
              name: credentials?.username
            }
          });
  
          if (user && credentials){
            const validPassword = await bcrypt.compare(credentials?.password, user.password); 
  
            if (validPassword){
              return {
                id: user.id,
                name: user.name,
              }
            }
          } 
        } catch(error){
          console.log(error)
        }
        return null
      },
      credentials: {
        username: {
          label: "Username",
          type: "text",
          placeholder: "jsmith",
        },
        password: {
          label: "Password",
          type: "password",
        },
      },

    })
  ],
  callbacks: {
    async jwt({token, user, profile, account, isNewUser}){

      // credentials provider:  Save the access token and refresh token in the JWT on the initial login
      if (user){
        const authUser = {id: user.id, name: user.name} as AuthUser;

        const accessToken = await jwtHelper.createAcessToken(authUser);
        const refreshToken = await jwtHelper.createRefreshToken(authUser);
        const accessTokenExpired = Date.now() /1000 + tokenOneDay;
        const refreshTokenExpired = Date.now() /1000 + tokenOnWeek;

        return {
          ...token, accessToken, refreshToken, accessTokenExpired, refreshTokenExpired,
          user: authUser
        }

      } else {
        if (token){
          // In subsequent requests, check access token has expired, try to refresh it
          if (Date.now() /1000 > token.accessTokenExpired){
            const verifyToken = await jwtHelper.verifyToken(token.refreshToken);

            if (verifyToken){
  
              const user = await prisma.user.findFirst({
                where: {
                  name: token.user.name
                }
              });

              if (user){
                const accessToken = await jwtHelper.createAcessToken(token.user);
                const accessTokenExpired = Date.now() /1000 + tokenOneDay;

                return {...token, accessToken, accessTokenExpired}
              } 
            }

            return {...token, error: "RefreshAccessTokenError"}
          }
        }
      }

      return token
    },

    async session({ session, token }){
      if (token){
        session.user = {
          name: token.user.name,
          userId: token.user.id
        }
      }
      session.error = token.error;
      return session;
    }
  }
};
``` 

Now that we've implemented NextAuth, we can easily protect unauthorized access from both the front-end and back-end of our application.

In t3-app, we can use the `protectedProcedure` to protect our API from unauthorized access by checking for a valid session. Additionally, we can use the `useSession()` hook to check whether a client is authorized to access certain features of the app.

I hope you found this tutorial helpful, the full source code is available on GitHub, so feel free to check it out and customize it to fit your needs. In the next tutorial, we'll explore how to build a multi-factor authentication to further bolster your app's security. Stay tuned! 👾👾👾👾  

Full code on [GitHub](https://github.com/Francoshum95/t3-auth-example)