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
- 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;
Install next-auth v5
npm i next-auth@beta
Add .env
file
Generate app secret
openssl rand -base64 32
Add env setting to .env file
AUTH_SECRET=<generated_secret>
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;
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' },
});
- 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$).*)'],
};
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;
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;
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)
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::
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
Then the client component for the Login form
There's a helpful guide on using
useFormState
hereblog.logrocket.com/understanding-r...