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
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? @/*
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
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
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
you can create your own secret by running
openssl rand -base64 32
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]
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 }
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,
}
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)
}
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)
}
}
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)
}
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>
}
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
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).*)',
],
}
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
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
login creds
username: kminchelle
password: 0lelplR
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)
This is a whole lot already, I will try it out