Next-Level Custom Authentication: Leveraging Next.js with NextAuth

Vitalismutwiri
4 min readMay 6, 2024

--

Recently i was creating custom authentication using Next.js and Next-auth and i found no clear explanation online on how to do it, so i customized my own, here is how i went around it,

Objective:

1. Authenticate custom Sign in with email and password using next-auth
2. Create session and store the userid
3. Use the userid to get your data
4. Restrict server and client components from being assessed if the user is not logged in.

Prerequisites

- Next js 14

Lets journey together

Assuming you have installed Next JS application, find how to get started here

Setup

To get started we install next-auth
npm install next-auth@beta

On the root create .env.local and add AUTH_SECRET.
Here is how to generate one
openssl rand -base64 32

Setting up Auth file

Create a Auth.js file on the root.
In this Auth.js, we will use Credential provider provided by NextAuth to get data and connect with our API.
I will give an instance with an example i have used.

Is great to know that my API returns userid so that i can use it to query data.
Also i have used JS so if you want to use TS kindly check their docs on types.

import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import axios from 'axios';
import { ZodError } from 'zod';
import { Signinschema } from '@/libs/forms/PostSchema';
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
email: {},
password: {},
},
authorize: async (credentials) => {
let user = null;
const { email, password } = await Signinschema.parseAsync(credentials);
// pass on the data to the object
const apidata = {
email: email,
password: password,
};
// use your data to sign in then query for user data
try {
const response = await axios.post(
'https://',
apidata
);
if (response.data && response.data.userId) {
let userId = response.data.userId;
user = (
await axios.get(
`https://${userId}`
)
).data.data.userProfile;
console.log(user);
return user;
} else {
console.error('userid not found');
}
} catch (error) {
if (error instanceof ZodError) {
// Return `null` to indicate that the credentials are invalid
return error;
}
console.error('Error occurred:', error);
}

},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) {
// User is available during sign-in
token.id = user.userId;
}
return token;
},
session({ session, token }) {
session.user.userId = token.id;
return session;
},
},
pages: {
signIn: '/sign-in',
},
});

First thing first we have the dependencies,
1. we have next-auth.
2. Then credentials provider from next auth.
3. I imported zod for validation and created my schema file.

import { z } from 'zod'
export const Signinschema = z.object({
email: z.string().min(3, 'Your email is requires').email('invalid Email address'),
password: z.string().min(3, 'Password is required')
})

In my credential provider, i used the Authorize callback, to authenticate users with provided username and password,

After getting the data i validated the data utilizing zod schema.
Then i made my api get request, with validated data.
Upon getting the response i confirmed the presence of userId on my response which in return used it to get data.
I wrapped all this in a try catch to catch all errors involved.

I then extended the session using callbacks jwt and session to get data provided by next-auth which includes my userID.

Here is how the jwt and session works

During sign-in, the jwt callback exposes the user’s profile information coming from the provider. You can leverage this to add the user’s id to the JWT token. Then, on subsequent calls of this API will have access to the user’s id via token.id. Then, to expose the user’s id in the actual session, you can access token.id in the session callback and save it on session.user.id.

That all on auth.js file.

Custom File
This is similar to one used in next Auth docs.

  <form
action={async (formData) => {
"use server"
await signIn("credentials", formData)
}}
>
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
<button>Sign In</button>
</form>

Remember it will not work if you are using it on client side or simply you are using use client

Handler file
I then created a folder on the app folder called api then created another folder called auth inside of api, then create another folder called […nextauth] inside auth, then create a file called route.ts or js inside […nextauth].

Here is the file example.

import { handlers } from "from base auth.js"; // Referring to the auth.ts we just created
export const { GET, POST } = handlers

Now to restrict a page from access, you can use middleware.ts on the root. The other option is to import auth we exported in auth.js and check if session exists, for server components


import React from 'react';
import { auth } from '../../../auth';
import { redirect } from 'next/navigation'

const page = async () => {
const session = await auth();
if(session){
redirect('/dashboard')
}

return (
<div>
//content
</div>
)
}

export default page

For client components use useSession from nextauth.

import { useSession } from 'next-auth/react';
// inside your function
const {data: session} = useSession({
required: true,
onUnauthenticated() {
redirect("/api/auth/signin")
}
})

That’s a wrap.

Congratulations on making to the end.
I would love to hear how you would have done it differently.

--

--