DEV Community

Shannon Clarke
Shannon Clarke

Posted on • Updated on

Getting started with NextAuthv5 Credentials Provider

Note that this tutorial heavily references the official Vercel tutorial for using NextJS and NextAuth https://nextjs.org/learn/dashboard-app/adding-authentication

This tutorial is for using NextAuth v5 to allow users to sign in via email/password on a custom signin page. You can also reference the official guide for transitioning from v4. Note that v5 is still in beta so documentation is still a work-in-progress. So, after having some difficulty myself, I wrote this guide to help others who may be trying to figure out setting up a standard email/password authentication flow from scratch

In this tutorial, we will:

  • Setup Next Auth with Credentials provider
  • Setup a custom login page
  • Use middleware to setup protected pages

First, create a new app

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode
  • Choose default options for each (including using the /src folder)
  • Remove default styles from /src/app/globals.css and leave the following
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Install next-auth v5

npm i next-auth@beta
Enter fullscreen mode Exit fullscreen mode

Add .env file

Generate app secret

openssl rand -base64 32

Add env setting to .env file

AUTH_SECRET=<generated_secret>
Enter fullscreen mode Exit fullscreen mode

Now that NextAuth supports route handlers (in app directory), it is often recommended to setup an api route (eg at /app/api/auth/[...nextauth]/route.ts) in order to utilize the built-in authentication pages as well as GET, POST handlers which are useful for the OAuth/email providers. However, since we're using the Credentials provider with a custom login page, we can use our own custom configuration (inspired by Vercel's dashboard tutorial)

First, let's create a configuration file for NextAuth and name itauth.config.ts

import type { NextAuthConfig } from 'next-auth';

export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
Enter fullscreen mode Exit fullscreen mode

Let's configure Next Auth integration to use Credentials Provider using auth.ts. Note that we're exporting helpful functions signIn, signOut and auth from the configuration. We'll be using these functions throughout the app

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  pages: {
    signIn: '/login',
  },
  providers: [
    Credentials({
      credentials: {
        email: {},
        password: {},
      },
      async authorize(credentials: any) {
        console.log(credentials);
        if (credentials) {
          //Return a valid user object
          return {
            id: '12345',
            name: 'testuser',
            email: credentials.email,
          };
        } else {
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user, account, profile, trigger, session }) {
      if (user && trigger === 'signIn') {
        token.user = user;
      }
      return token;
    },
    async session({ session, token }) {
      session.user = token.user as any;
      return session;
    },
  },
  session: { strategy: 'jwt' },
});
Enter fullscreen mode Exit fullscreen mode
  • Note that we are using jwt strategy because NextAuth does not by default support storing user sessions and tokens in the database when using CredentialsProvider
  • We have set /login page as our signin page for NextAuth. This will be used when NextAuth is redirecting the user after logout

Add a middleware which will redirect the user based on their logged in status and protect authenticated pages

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
Enter fullscreen mode Exit fullscreen mode

Now in order to test, let’s create a login page at /login/page.tsx

We’ll use a server action in order to trigger the signIn function provided by NextAuth

import { signIn } from '@/auth';

const Login = () => {
  const handleSignIn = async (formData: FormData) => {
    'use server';
    const email = formData.get('email');
    const password = formData.get('password');
    await signIn('credentials', {
      email,
      password,
    });
  };
  return (
    <div>
      <h1>Login</h1>
      <form action={handleSignIn}>
        <input
          className="border border-gray-400 block"
          name="email"
          type="email"
        />
        <input
          className="border border-gray-400 block"
          name="password"
          type="password"
        />
        <button type="submit">Sign In</button>
      </form>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

and then a dashboard page at /dashboard/page.tsx

we’ll display some information from the current user session and we’ll use a server action in order to trigger the signOut function provided by NextAuth

import { signOut } from '../../auth';
import { PowerIcon } from '@heroicons/react/24/outline';
import { auth } from '../../auth';

const Dashboard = async () => {
  const session = await auth();
  return (
    <div>
      <h1>Dashboard</h1>
      <div>{session ? <p>Welcome back, {session?.user?.name}</p> : null}</div>
      <form
        action={async () => {
          'use server';
          await signOut();
        }}
      >
        <button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
          <PowerIcon className="w-6" />
          <div className="hidden md:block">Sign Out</div>
        </button>
      </form>
    </div>
  );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

We can now test the functionality by entering a valid email and password in the login form on /login

If successful, you should be redirected to the /dashboard

On the dashboard page, you should be able to test the logout functionality. If successful, you will be redirected to the /login page

You can also test whether or not the page protection is working by trying to visit /dashboard page when logged out. You should be redirected to the /login page

So far, we’re able to allow a user to create a session by entering their user information but that’s not exactly a real-world scenario where you would authenticate the user credentials against a database or other authentication service.

In the next tutorial, we will connect to database using DrizzleORM and Planetscale to fetch user details

Please let me know if you'd like an example repo of the above and if I can clarify any of the steps, please don't hesitate to let me know

Top comments (3)

Collapse
 
hiswordllc profile image
HisWordLLC • Edited

Out of curiosity - were you also unable to get useFormState working for login? I've been banging my head against my keyboard for a couple hours, and was so happy to come across your solution. Thank you.

My signUp component worked just fine with useFormState, which doesn't use (auth) but sits under the same route.

I'm excited for the other gotchas. ::pain-harold::

Collapse
 
shannonajclarke profile image
Shannon Clarke

Hey @hiswordllc yes I'm able to make it work but keep in mind that would require changing the login page to a client component. See an example below

First the server action


import { signIn } from '@/auth';

export type MessageState = {
  message: string | null;
};

export async function loginUser(
  previousState: MessageState,
  formData: FormData,
) {
  try {
    await signIn('credentials', {
      email: formData.get('email'),
      password: formData.get('password'),
    });

    return { message: 'Success' };
  } catch (error) {
    return {
      message: 'Sorry an error occurred',
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Then the client component for the Login form

'use client';

import { useFormState } from 'react-dom';
import { loginUser, MessageState } from './actions';

const initialState: MessageState = {
  message: null,
};

const Login = () => {
  const [state, formAction] = useFormState(loginUser, initialState);

  return (
    <div>
      <h1>Login</h1>
      <form action={formAction}>
        <input
          className="border border-gray-400 block"
          name="email"
          type="email"
        />
        <input
          className="border border-gray-400 block"
          name="password"
          type="password"
        />
        <button type="submit">Sign In</button>
        <div>{state?.message}</div>
      </form>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
shannonajclarke profile image
Shannon Clarke

There's a helpful guide on using useFormState here
blog.logrocket.com/understanding-r...