DEV Community

Cover image for Next Auth boilerplate TS
Syed Faran Mustafa
Syed Faran Mustafa

Posted on

Next Auth boilerplate TS

So recently I started working on a NextJS project with TS and was using next-auth for authentication the project was using typescript mainly so using next-auth was a challenge because the documentation consisted mainly of the javascript implementation and didn't find any typescript implemented and so have implemented the next-auth typescript implementation myself would like to share with others too and appreciate if anyone wants to contribute and further improvise it.

Starting off

I'll show you guys what I did and main parts of it in this article, You can skip these steps and head onto the repo to directly check the code https://github.com/FaranMustafa/next-auth-ts-boilerplate

Steps

1. Start with Nextjs

Create your next application run the command below on the terminal

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

after that, you will see the following prompts

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
Enter fullscreen mode Exit fullscreen mode

You can select the prompts according to your needs just need to check with yes on using typescript and tailwind as I am using it for styling on the login page.

You can then head on to the folder and run nextjs server that would be available on http://localhost:3000 by default

npm run dev
Enter fullscreen mode Exit fullscreen mode

now you have successfully set up and started the nextjs project 🚀

2. Add Next JS to the project

Starting of with installing next-auth you can check with the getting started documentation link

npm install next-auth
Enter fullscreen mode Exit fullscreen mode

after that add following environment in .env file

# next auth secret created by open ssl 
NEXTAUTH_SECRET=8xvug0aweP+j06DbfOLH9fAU7Bf2nLBLnYHQiFPB9Pc=

#local 
NEXTAUTH_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="https://dummyjson.com"

# deveploment
NEXTAUTH_DEBUG=true

Enter fullscreen mode Exit fullscreen mode

you can create your own secret by running

openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

also, change your NEXT_PUBLIC_API_URL to your backend authentication API for this implementation I am currently using dummy JSON API you could also implement your own APIs in nextjs.

3. Setting up next auth

To add NextAuth.js to a project create a file called [...nextauth].js in app/api/auth. This contains the dynamic route handler which will contain global NextAuth.js configurations. You can also header over to it's documentation too link

now your folder would look like this

/app/api/auth/[...nextauth]
Enter fullscreen mode Exit fullscreen mode

Now create your route.ts file

// Path: app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import { authOptions } from './options'
import { AuthOptions } from 'next-auth'

const handler = NextAuth(authOptions as AuthOptions)

export { handler as GET, handler as POST }

Enter fullscreen mode Exit fullscreen mode

auth options are passed for the configuration of the routes

now create an options file in that folder

// Path: app/api/auth/[...nextauth]/option.ts
import CredentialsProvider from 'next-auth/providers/credentials'
import { login, refreshToken } from '@/services/auth'

// interfaces for credentials

// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export const authOptions = {
  // https://next-auth.js.org/configuration/providers
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        username: { label: 'User', type: 'text' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials) {
          throw new Error('Credentials not provided')
        }
        let loginResponse = await login({
          username: credentials.username,
          password: credentials.password,
        })

        if (loginResponse.status === 200) {
          return {
            id: loginResponse.data.id,
            status: 'success',
            data: loginResponse.data,
          }
        }
        if (loginResponse.status > 200) {
          throw new Error(loginResponse.data.message)
        }

        throw new Error('Login failed')
      },
    }),
  ],
  session: {
    // Use JSON Web Tokens for session instead of database sessions.
    // This option can be used with or without a database for users/accounts.
    // Note: `strategy` should be set to 'jwt' if no database is used.
    strategy: 'jwt',
  },
  // You can define custom pages to override the built-in ones. These will be regular Next.js pages
  // so ensure that they are placed outside of the '/api' folder, e.g. signIn: '/auth/mycustom-signin'
  // The routes shown here are the default URLs that will be used when a custom
  // pages is not specified for that route.
  // https://next-auth.js.org/configuration/pages
  pages: {
    signIn: '/login',
    error: '/login', // Error code passed in query string as ?error=
    verifyRequest: '/login', // (used for check email message)
    signUp: '/signup',
  },
  // Callbacks are asynchronous functions you can use to control what happens
  // when an action is performed.
  // https://next-auth.js.org/configuration/callbacks
  callbacks: {
    async session(payload: any) {
      const { token } = payload
      return {
        ...token,
      }
    },
    async jwt(payload: any) {
      const { token: tokenJWT, user: userJWT, account, trigger } = payload

      // TODO: check for user object is the way for it
      // ** this is the way to check if the user is logged in or invoked by the trigger login and creds
      if (trigger === 'signIn' && account.type === 'credentials') {
        let user = userJWT.data
        let status = userJWT.status
        let tokenData = userJWT.data
        let token = {
          accessToken: tokenData.token,
          refreshToken: tokenData.refreshToken,
          tokenExpires: tokenData.tokenExpires,
        }
        let role = user.role

        try {
          return {
            token,
            user,
            status,
            role,
          }
        } catch (error) {
          throw new Error('Error setting up session')
        }
      }

      // TODO: check if the token expired and refresh token
      const shouldRefreshTime = Math.round(
        tokenJWT.token.tokenExpires - 60 * 60 * 1000 - Date.now()
      )

      if (shouldRefreshTime < 0) {
        try {
          let payload = {}
          let headers = {
            'Content-Type': 'application/json',
            Authorization: tokenJWT.token.refreshToken,
          }

          let ResponseTokenRefresh = await refreshToken(payload, headers)
          if (ResponseTokenRefresh.data.status === 'success') {
            let data = ResponseTokenRefresh.data.data
            let token = {
              accessToken: data.token,
              refreshToken: data.refreshToken,
              tokenExpires: data.tokenExpires,
            }
            return {
              ...tokenJWT,
              token,
            }
          }
        } catch (error) {
          throw new Error('Token refresh failed')
        }
      }

      // ** pass the information to the session on normal invocation
      return { ...tokenJWT }
    },
  },
  jwt: {
    // A secret to use for key generation (you should set this explicitly)
    secret: process.env.NEXTAUTH_SECRET,
    // Set to true to use encryption (default: false)
    // encryption: true,
    // You can define your own encode/decode functions for signing and encryption
    // if you want to override the default behaviour.
    // encode: async ({ secret, token, maxAge }) => {},
    // decode: async ({ secret, token, maxAge }) => {},
  },
  secret: process.env.NEXTAUTH_SECRET,
  debug: process.env.NEXTAUTH_DEBUG || false,
}

Enter fullscreen mode Exit fullscreen mode

This might be overwhelming but I have added comments and related links too might help

you can skip this I just created this for my ease, In this we will make a get session function to get server side session and don't have pass authOptions cause that is required

// Path: app/api/auth/[...nextauth]/auth.ts
import type {
  GetServerSidePropsContext,
  NextApiRequest,
  NextApiResponse,
} from 'next'
import type { NextAuthOptions } from 'next-auth'
import { getServerSession } from 'next-auth'
import { authOptions } from './options'

// You'll need to import and pass this
// to `NextAuth` in `app/api/auth/[...nextauth]/route.ts`
export const config = {
  ...authOptions,
} as NextAuthOptions

// Use it in server contexts
export function auth(
  ...args:
    | [GetServerSidePropsContext['req'], GetServerSidePropsContext['res']]
    | [NextApiRequest, NextApiResponse]
    | []
) {
  return getServerSession(...args, config)
}


Enter fullscreen mode Exit fullscreen mode

Now using this you can import this function anywhere on the server side and get the session.

Now that we are done with these global setting from the next Auth we will make our services

now create a folder service and create a index.ts file in this we will define our interceptors and CRUD operations

// Path: app/services/index.ts
// class API client
import { getSession } from 'next-auth/react'
import axios from 'axios'
import { redirect } from 'next/navigation'

axios.interceptors.response.use(
  function (response) {
    return response
  },
  function (error) {
    if (error.response?.status === 401) {
      if (typeof window !== 'undefined') {
        window.location.href = '/login'
      } else {
        // Handle server-side redirection
        redirect('/login')
      }
    }

    return Promise.reject(error)
  }
)

export class ApiClient {
  static baseUrl = process.env.NEXT_PUBLIC_API_URL
  static async get(path: string, headers = {}) {
    const session = await getSession()
    let config = {
      method: 'get',
      url: this.baseUrl + path,
      headers: {
        // use this for auth is true
        Authorization: `Bearer ${session?.token}`,
        ...headers,
      },
    }
    return axios.request(config)
  }

  static async post(path: string, payload = {}, headers = {}) {
    const session = await getSession()
    const data = payload
    let config = {
      method: 'post',
      maxBodyLength: Infinity,
      url: this.baseUrl + path,
      headers: {
        //check to
        Authorization: `Bearer ${session?.token}`,
        ...headers,
      },
      data: data,
    }
    return axios.request(config)
  }

  static async delete(path: string, payload = {}, headers = {}) {
    const session = await getSession()
    let data = payload
    let config = {
      method: 'delete',
      maxBodyLength: Infinity,
      url: this.baseUrl + path,
      headers: {
        Authorization: `Bearer ${session?.token}`,
        'Content-Type': 'application/json',
        ...headers,
      },
      data: data,
    }
    return axios.request(config)
  }

  static async put(path: string, payload = {}) {
    const session = await getSession()

    let data = JSON.stringify(payload)
    let config = {
      method: 'put',
      maxBodyLength: Infinity,
      url: this.baseUrl + path,
      headers: {
        Authorization: `Bearer ${session?.token}`,
        'Content-Type': 'application/json',
      },
      data: data,
    }
    return axios.request(config)
  }

  static async patch(path: string, payload = {}) {
    const session = await getSession()

    let data = JSON.stringify(payload)
    let config = {
      method: 'patch',
      maxBodyLength: Infinity,
      url: this.baseUrl + path,
      headers: {
        Authorization: `Bearer ${session?.token}`,
        'Content-Type': 'application/json',
      },
      data: data,
    }
    return axios.request(config)
  }
}

Enter fullscreen mode Exit fullscreen mode

now after that we will create our auth services by creating a auth.ts file in same folder

// Path: services/auth.ts
import { ApiClient } from '.'

interface ILogin {
  username: string
  password: string
  expiresInMins?: number // optional
}

interface IUserInfoHeaders {
  Authorization: string
  // you can add more headers here
}

interface IRefreshTokenHeaders {
  'Content-Type': string
  Authorization: string
}

interface IRefreshTokenBody {
  expiresInMins?: number
}

export const login = (payload: ILogin) => {
  return ApiClient.post(`/auth/login`, payload, {})
}
export const getUserInfo = (headers: IUserInfoHeaders) => {
  return ApiClient.get(`/auth/me`, headers)
}

export const refreshToken = (
  payload: IRefreshTokenBody,
  headers: IRefreshTokenHeaders
) => {
  console.log('refreshToken', payload, headers)
  return ApiClient.post(`/auth/refresh`, payload, headers)
}

Enter fullscreen mode Exit fullscreen mode

After that we done with this we have to provide our AuthProvider to get the session on the client side

create a file AuthProvider in context folder


// Path: app/context/AuthProvider.tsx

'use client'
import { SessionProvider } from 'next-auth/react'
import React from 'react'

export default function AuthProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <SessionProvider>{children}</SessionProvider>
}

Enter fullscreen mode Exit fullscreen mode

and then add it to your layout.tsx file

// Path: app/layout.tsx
import type { Metadata } from 'next'
import AuthProvider from './context/AuthProvider'
import { Inter } from 'next/font/google'

import '../styles/globals.css'

const inter = Inter({ subsets: ['latin'] })

import * as AppConstant from '../utils/constants/app-constants'

interface RootLayoutProps {
  children: React.ReactNode
  params: { locale: string }
}

export const metadata: Metadata = AppConstant.metaData

const RootLayout: React.FC<RootLayoutProps> = (props) => {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AuthProvider>{props.children}</AuthProvider>
      </body>
    </html>
  )
}

export default RootLayout

Enter fullscreen mode Exit fullscreen mode

After done with all this we just now have to make private routes for that we will use middleware and I have made my own custom middleware you simplify that too head on to https://next-auth.js.org/configuration/nextjs#middleware

// Path: app/middleware.ts
import { withAuth } from 'next-auth/middleware'
import { NextResponse } from 'next/server'

export default withAuth(
  // `withAuth` augments your `Request` with the user's token.
  function middleware(request) {
    let token = request.nextauth?.token?.token
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }

    // if at login page and user is logged in, redirect to the dashboard
    if (request.nextUrl.pathname.startsWith('/login') && token) {
      return NextResponse.redirect(new URL('/', request.url))
    }
  }
)

export const config = {
  matcher: [
    // Match all routes except the ones that start with /login and api and the static folder
    '/((?!api|_next/static|_next/image|images|favicon.ico|login).*)',
  ],
}

Enter fullscreen mode Exit fullscreen mode

now that all the configuration for next-auth is done we now head on and make a sample login page to see if everything is working as we expected.

4. Login page

So I have created a login page using Formik for validation and adding structure to the form and also added tailwind so to follow along to this guide you have to do the following

Add formik to your project

npm install formik --save
Enter fullscreen mode Exit fullscreen mode

and if you haven't configured your project with tailwind head to this link https://tailwindcss.com/docs/installation

add this to your login page

// Path: app/login/index.tsx

'use client'

import React from 'react'
import { signIn } from 'next-auth/react'

// form
import { Formik, Form } from 'formik'

// navigation
import { useSearchParams } from 'next/navigation'

interface ILoginFormValues {
  username: string
  password: string
}

const initialLoginForm: ILoginFormValues = {
  username: '',
  password: '',
}

const LoginFormComponent = () => {
  const searchParams = useSearchParams()
  const callbackUrl = searchParams.get('callbackUrl')
  const errorMessage = searchParams.get('error')
  const successMessage = searchParams.get('success')

  React.useEffect(() => {
    if (errorMessage) {
      alert(
        'error' + 'Login Error' + errorMessage
        // 'Please check your email and password and try again. If you are still having trouble, please contact support.'
      )
    }
    if (successMessage) {
      alert('success' + 'Login Success' + successMessage)
    }
  }, [errorMessage, successMessage])

  const submitLoginForm = async (values: ILoginFormValues) => {
    console.log('values', values)
    try {
      await signIn('credentials', {
        username: values.username,
        password: values.password,
        redirect: true,
        callbackUrl: callbackUrl ?? '/',
      })
    } catch (err) {
      console.error('error login form' + JSON.stringify(err))
    }
  }

  return (
    <div className="flex items-center h-screen w-full">
      <div className="w-full bg-white rounded shadow-lg p-8 m-4 md:max-w-sm md:mx-auto">
        <span className="block w-full text-xl uppercase font-bold mb-4">
          Login
        </span>
        <Formik
          initialValues={initialLoginForm}
          // validation schema using yup
          validate={(values) => {
            const errors: Partial<ILoginFormValues> = {}
            if (!values.username) {
              errors.username = 'Required'
            }
            if (!values.password) {
              errors.password = 'Required'
            }
            return errors
          }}
          onSubmit={submitLoginForm}
        >
          {({
            values,
            errors,
            touched,
            handleChange,
            handleBlur,
            handleSubmit,
            isValid,
          }) => (
            <Form onSubmit={handleSubmit} className="mb-4">
              <div className="mb-4 md:w-full">
                <label htmlFor="email" className="block text-xs mb-1">
                  Username or Email
                </label>
                <input
                  className="w-full border rounded p-2 outline-none focus:shadow-outline"
                  type="text"
                  name="username"
                  id="username"
                  placeholder="Enter Username"
                  value={values.username}
                  onChange={handleChange}
                  onBlur={handleBlur}
                />
                {touched.username && errors.username && (
                  <span className="error-label">{errors.username}</span>
                )}
              </div>
              <div className="mb-6 md:w-full">
                <label htmlFor="password" className="block text-xs mb-1">
                  Password
                </label>
                <input
                  className="w-full border rounded p-2 outline-none focus:shadow-outline"
                  type="password"
                  name="password"
                  id="password"
                  value={values.password}
                  onChange={handleChange}
                  placeholder="Password"
                />
                {touched.password && errors.password && (
                  <span className="error-label">{errors.password}</span>
                )}
              </div>
              <button
                disabled={!isValid}
                className="bg-green-500 hover:bg-green-700 text-white uppercase text-sm font-semibold px-4 py-2 rounded"
              >
                Login
              </button>
            </Form>
          )}
        </Formik>
        <a className="text-blue-700 text-center text-sm" href="/login">
          Forgot password?
        </a>
      </div>
    </div>
  )
}

export default LoginFormComponent

Enter fullscreen mode Exit fullscreen mode

login creds

  username: kminchelle
  password: 0lelplR
Enter fullscreen mode Exit fullscreen mode

Hurray 🎉 you are done with the guide if you have any queries or ideas for improvement let me know in the comments, Thank you for reading this article 😊.

Top comments (1)

Collapse
 
sam4rano profile image
Oyerinde Samuel

This is a whole lot already, I will try it out