Building an App with GitHub and Credentials Authentication in Next.js 15 with Sanity

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;

Feb 26, 2025 - 01:44
 0
Building an App with GitHub and Credentials Authentication in Next.js 15 with Sanity

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