Authentication is a crucial part of any modern application. In this guide, we will walk through how to set up authentication using GitHub and credentials (email/password) in a Next.js 15 app, while persisting user data in Sanity.
Tech Stack
- Next.js 15: A React framework for building web applications.
- NextAuth.js: Authentication library for Next.js.
- Sanity: A headless CMS to store user data.
- Tailwind CSS (optional): For styling.
Prerequisites
Before getting started, ensure you have:
- A GitHub OAuth App set up.
- A Sanity project created.
- A Next.js 15 app initialized.
Step 1: Setting Up Next.js and Installing Dependencies
First, create a new Next.js 15 project and install the required dependencies:
npx create-next-app@latest my-app --ts
cd my-app
npm install next-auth @sanity/client dotenv
Step 2: Configuring NextAuth.js
NextAuth.js handles authentication. Create an auth.ts
file inside the lib
directory:
import { AUTHOR_BY_EMAIL, AUTHOR_BY_GITHUB_ID_QUERY } from '@/sanity/lib/queries';
import { writeClient } from '@/sanity/lib/write-client';
import NextAuth, { Profile, User } from 'next-auth';
import GitHub from 'next-auth/providers/github';
import { client } from './sanity/lib/client';
import Credentials from 'next-auth/providers/credentials';
import { signInSchema } from './lib/zod';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub,
Credentials({
credentials: {
email: {},
password: {},
action: { label: 'Action', type: 'text' },
},
authorize: async (credentials) => {
try {
let user = null;
const { email, password, action='login' } = await signInSchema.parseAsync(credentials);
if (action === 'register') {
const existingUser = await writeClient.fetch(AUTHOR_BY_EMAIL, { email });
if (existingUser) {
throw new Error('User already exists');
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await writeClient.create({
_type: 'author',
email,
password: hashedPassword,
});
return { ...user, id: user._id };
}
// logic to verify if the user exists
user = await client.fetch(AUTHOR_BY_EMAIL, {
email
});
if (!user) {
throw new Error('Invalid credentials.');
}
const hashedPassword = await bcrypt.hash('password321', 10);
await writeClient
.patch(user._id)
.set({ password: hashedPassword })
.commit();
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new Error("Invalid password");
}
return { ...user, id: user._id };
} catch (error) {
if (error instanceof z.ZodError) {
return null;
}
}
}
})
],
callbacks: {
async signIn({ user: { name, email, image }, account, profile }) {
console.log("jjjjjj")
if (account?.provider === 'github') {
const { id, login, bio } = profile || {};
const existingUser = await client.fetch(AUTHOR_BY_GITHUB_ID_QUERY, {
id
});
if (!existingUser) {
await writeClient.withConfig({ useCdn: false }).create({
_type: 'author',
id,
name,
username: login,
email,
image,
bio: bio || ''
});
}
return true;
}
return true;
},
async jwt( { token, account, profile, user }) {
console.log("gggg")
if (account?.provider === 'github') {
if (account && profile) {
const gitHubUser = await client.withConfig({ useCdn: false }).fetch(AUTHOR_BY_GITHUB_ID_QUERY, {
id: profile?.id
});
token.id = gitHubUser?._id;
}
return token;
} else {
if (user) {
token.id = user.id;
}
return token;
}
},
async session({ session, token }) {
(session.user as any).id = token.id;
return session;
}
}
});
Step 3: Setting Up API Route
Create a NextAuth API route in app/api/auth/[...nextauth]/route.ts
:
import { handlers } from "@/auth" // Referring to the auth.ts we just created
export const { GET, POST } = handlers
Step 4: Configuring Sanity Schema for Users
Modify your sanity/schema.json
in your Sanity project:
[...,{
"name": "author",
"type": "document",
"attributes": {
"_id": {
"type": "objectAttribute",
"value": {
"type": "string"
}
},
"_type": {
"type": "objectAttribute",
"value": {
"type": "string",
"value": "author"
}
},
"_createdAt": {
"type": "objectAttribute",
"value": {
"type": "string"
}
},
"_updatedAt": {
"type": "objectAttribute",
"value": {
"type": "string"
}
},
"_rev": {
"type": "objectAttribute",
"value": {
"type": "string"
}
},
"id": {
"type": "objectAttribute",
"value": {
"type": "number"
},
"optional": true
},
"name": {
"type": "objectAttribute",
"value": {
"type": "string"
},
"optional": true
},
"username": {
"type": "objectAttribute",
"value": {
"type": "string"
},
"optional": true
},
"email": {
"type": "objectAttribute",
"value": {
"type": "string"
},
"optional": true
},
"image": {
"type": "objectAttribute",
"value": {
"type": "string"
},
"optional": true
},
"bio": {
"type": "objectAttribute",
"value": {
"type": "string"
},
"optional": true
},
"password": {
"type": "objectAttribute",
"value": {
"type": "string"
},
"optional": true
}
}
}]
Step 5: Adding Authentication to the Frontend
Modify app/login/page.tsx
to show the login form:
import { Login } from './Login';
const Page = () => {
return <Login />;
};
export default Page;
Login.tsx
'use client';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useActionState, useEffect, useState } from 'react';
import { signInAction, signInWithGithub } from './actions';
import { PasswordFields } from './PasswordFields';
const initState = {
status: false,
errors: []
};
export function Login() {
const [isRegistering, setIsRegistering] = useState(false);
const { data: session, status } = useSession();
const [state, action] = useActionState(signInAction, initState);
const [isPasswordValid, setIsPasswordValid] = useState(true);
const router = useRouter();
useEffect(() => {
if (state.status || status === 'authenticated') {
router.push('/');
}
}, [status, state.status, router]);
return (
<>
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md bg-white rounded-[30px] shadow-custom-top border-0 p-8 space-y-8">
<h2 className="text-2xl font-normal">{!isRegistering ? `Sign In` : `Register`}</h2>
<form action={action} className="space-y-6">
<input type="email" name="email" placeholder="Email" className="w-full p-2 rounded-[30px] border border-gray-300" />
<PasswordFields isRegistering={isRegistering} onValidityChange={setIsPasswordValid} />
{Boolean(state.errors.length) && (
<div className="text-red-500">
{state.errors.map((error, i) => (
<ul key={i}>
<li>{error}</li>
</ul>
))}
</div>
)}
<button type="submit" className={`w-full p-2 rounded-[30px] text-white ${!isPasswordValid ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-500'}`} disabled={!isPasswordValid}>
{isRegistering ? 'Register' : 'Sign In'}
</button>
</form>
<button onClick={() => setIsRegistering(!isRegistering)} className="text-blue-500 underline bg-transparent border-none p-0 cursor-pointer">
{!isRegistering ? `Register instead` : `Login instead`}
</button>
<form action={signInWithGithub}>
<button type="submit" className="py-2 px-4 max-w-md flex justify-center items-center bg-gray-600 hover:bg-gray-700 focus:ring-gray-500 focus:ring-offset-gray-200 text-white w-full transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="mr-2" viewBox="0 0 1792 1792">
<path d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5q0 251-146.5 451.5t-378.5 277.5q-27 5-40-7t-13-30q0-3 .5-76.5t.5-134.5q0-97-52-142 57-6 102.5-18t94-39 81-66.5 53-105 20.5-150.5q0-119-79-206 37-91-8-204-28-9-81 11t-92 44l-38 24q-93-26-192-26t-192 26q-16-11-42.5-27t-83.5-38.5-85-13.5q-45 113-8 204-79 87-79 206 0 85 20.5 150t52.5 105 80.5 67 94 39 102.5 18q-39 36-49 103-21 10-45 15t-57 5-65.5-21.5-55.5-62.5q-19-32-48.5-52t-49.5-24l-20-3q-21 0-29 4.5t-5 11.5 9 14 13 12l7 5q22 10 43.5 38t31.5 51l10 23q13 38 44 61.5t67 30 69.5 7 55.5-3.5l23-4q0 38 .5 88.5t.5 54.5q0 18-13 30t-40 7q-232-77-378.5-277.5t-146.5-451.5q0-209 103-385.5t279.5-279.5 385.5-103zm-477 1103q3-7-7-12-10-3-13 2-3 7 7 12 9 6 13-2zm31 34q7-5-2-16-10-9-16-3-7 5 2 16 10 10 16 3zm30 45q9-7 0-19-8-13-17-6-9 5 0 18t17 7zm42 42q8-8-4-19-12-12-20-3-9 8 4 19 12 12 20 3zm57 25q3-11-13-16-15-4-19 7t13 15q15 6 19-6zm63 5q0-13-17-11-16 0-16 11 0 13 17 11 16 0 16-11zm58-10q-2-11-18-9-16 3-14 15t18 8 14-14z"></path>
</svg>
Sign in with GitHub
</button>
</form>
</div>
</div>
</>
);
}
action.ts
'use server';
import { signIn } from '@/auth';
export const signInWithGithub = async () => {
await signIn('github');
};
export const signInAction = async (prevState: any, formData: FormData) => {
let result = {
status: true,
errors: [] as string[]
};
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const repeatPassword = formData.get('repeatPassword') as string;
if (repeatPassword && password !== repeatPassword) {
result.status = false;
result.errors.push('Passwords do not match.');
return result;
}
try {
const res = await signIn('credentials', {
redirect: false,
email,
password,
action: Boolean(repeatPassword)? 'register' : 'login'
});
if (res?.error) {
result.status = false;
result.errors.push(res.error);
return result;
}
} catch (error) {
result.status = false;
result.errors.push('Username and password does not match or user does not exist!');
}
return result;
};
components/Logout.tsx
'use client';
import { signOut } from "next-auth/react";
export function LogoutButton() {
const handleLogout = async () => {
localStorage.setItem('logout', Date.now().toString());
await signOut({ redirectTo: '/' });
};
return (
<button
onClick={handleLogout}
className="bg-blue-500 text-white px-4 py-2 rounded-lg"
>
Logout
</button>
);
}
Conclusion
By following these steps, you have successfully built an authentication system in Next.js 15 using NextAuth.js, GitHub OAuth, and credentials authentication while persisting users in Sanity. This setup provides a scalable and secure foundation for your app. Find the completed code here
Top comments (0)