Authentication is a cornerstone of almost every modern web application. For Next.js developers, NextAuth.js offers a robust, flexible, and easy-to-use solution for handling various authentication strategies. In this tutorial, we'll walk through setting up NextAuth.js in a Next.js project using TypeScript, focusing on a basic "credentials" provider (username/password) for demonstration purposes.
Why NextAuth.js?
Flexible: Supports various authentication providers (OAuth, email, credentials, etc.).
Secure: Handles sessions, JWTs, and secure cookies out of the box.
Easy to Use: Simple API for common authentication flows.
Built for Next.js: Integrates seamlessly with Next.js API routes and getServerSideProps
.
Let's dive in!
Prerequisites
Before we start, make sure you have:
Node.js installed
Familiarity with Next.js and React basics
Basic understanding of TypeScript
Step 1: Set Up Your Next.js Project
First, let's create a new Next.js project with TypeScript. Open your terminal and run:
npx create-next-app@latest nextauth-typescript-tutorial --typescript
cd nextauth-typescript-tutorial
Step 2: Install NextAuth.js
Now, install the next-auth package:
npm install next-auth
# or
yarn add next-auth
Step 3: Configure Environment Variables
NextAuth.js requires a secret for signing tokens and an optional database URL (if you plan to use a database adapter, which we won't cover in this basic example but is crucial for production).
Create a .env.local file at the root of your project:
Code snippet
# .env.local
NEXTAUTH_SECRET=YOUR_VERY_RANDOM_SECRET_STRING_HERE
# You can generate a random string using: openssl rand -base64 32
# For a quick dev secret, any sufficiently long random string will do.
Important: Never share your NEXTAUTH_SECRET publicly. For production, ensure this is a strong, randomly generated string.
Step 4: Create the NextAuth.js API Route
NextAuth.js operates via a dynamic API route. Create a new file at pages/api/auth/[...nextauth].ts.
This file will contain all your NextAuth.js configuration.
// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export default NextAuth({
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g., "Sign in with...")
name: 'Credentials',
// `credentials` is used to generate a form on the sign in page.
// You can specify which fields should be submitted, for example:
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials, req) {
// Add logic here to look up the user from the credentials supplied
// This is where you would connect to your database or authentication service.
// For this example, we'll hardcode a user.
if (credentials?.username === 'user' && credentials?.password === 'password') {
const user = { id: '1', name: 'J Smith', email: 'jsmith@example.com' };
// Any object returned will be saved in `user` property of the JWT
return user;
} else {
// If you return null then an error will be displayed advising the user to check their details.
return null;
// You can also throw an error to trigger an error page or redirect.
}
},
}),
// ... add more providers here (e.g., GitHubProvider, GoogleProvider)
],
// Optional: Add callbacks for more control over authentication flow
callbacks: {
async jwt({ token, user }) {
// The `user` object is only available on the first login (sign-in)
// or if you explicitly return it from `authorize`
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
// Send properties to the client, such as an `id` from a JWT.
session.user.id = token.id as string;
return session;
},
},
// Optional: Pages for custom login, error, etc.
pages: {
signIn: '/auth/signin', // Custom sign-in page
// signOut: '/auth/signout',
// error: '/auth/error', // Error code passed in query string as ?error=
// verifyRequest: '/auth/verify-request', // Used for check email page
// newUser: '/auth/new-user' // If set will redirect to this page after a new account is created
},
// Debug mode for development
debug: process.env.NODE_ENV === 'development',
});
Explanation of pages/api/auth/[...nextauth].ts:
NextAuth({...}): The main configuration object for NextAuth.js.
providers: An array where you define all the authentication methods you want to support.
CredentialsProvider: Allows users to sign in with a username and password (or any arbitrary credentials).
name: The label shown on the sign-in button.
credentials: Defines the input fields for your sign-in form.
authorize: This is the core function where you validate the credentials. In a real application, you'd query your database here to find a matching user. If authentication is successful, return a User object; otherwise, return null.
callbacks: Highly customizable functions that allow you to control what happens at different stages of the authentication flow.
jwt({ token, user }): This callback is called whenever a JSON Web Token (JWT) is created or updated. You can add custom properties to the token here (e.g., user ID).
session({ session, token }): This callback is called whenever a session is checked. You can expose properties from the token to the session object, which is then available on the client-side.
pages: Allows you to define custom pages for various authentication states (sign-in, sign-out, error, etc.). This is crucial for a good user experience.
debug: Set to true in development for helpful console logging.
Step 5: Create a Custom Sign-In Page
Since we specified /auth/signin as our custom sign-in page in the pages configuration, let's create it.
Create pages/auth/signin.tsx:
// pages/auth/signin.tsx
import { signIn, getProviders } from 'next-auth/react';
import { GetServerSidePropsContext } from 'next';
import { BuiltInProviderType } from 'next-auth/providers/index';
import { ClientSafeProvider, LiteralUnion } from 'next-auth/react/types';
interface SignInPageProps {
providers: Record<LiteralUnion<BuiltInProviderType, string>, ClientSafeProvider>;
}
export default function SignIn({ providers }: SignInPageProps) {
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h1>Sign In</h1>
{providers &&
Object.values(providers).map((provider) => {
if (provider.id === 'credentials') {
return (
<div key={provider.id} style={{ marginTop: '1rem' }}>
<h2>Sign in with {provider.name}</h2>
<form
onSubmit={async (e) => {
e.preventDefault();
const username = (document.getElementById('username') as HTMLInputElement).value;
const password = (document.getElementById('password') as HTMLInputElement).value;
const result = await signIn('credentials', {
username,
password,
redirect: false, // Prevent redirect for custom error handling
callbackUrl: '/', // Redirect to home page on success
});
if (result?.error) {
alert(result.error); // Basic error display
} else if (result?.ok) {
window.location.href = result.url || '/'; // Redirect manually on success
}
}}
>
<div>
<label htmlFor="username">Username:</label>
<input type="text" id="username" name="username" required />
</div>
<div style={{ marginTop: '0.5rem' }}>
<label htmlFor="password">Password:</label>
<input type="password" id="password" name="password" required />
</div>
<button type="submit" style={{ marginTop: '1rem', padding: '0.5rem 1rem' }}>
Sign In
</button>
</form>
</div>
);
}
// You can render other providers here if you add them (e.g., Google, GitHub)
// return (
// <div key={provider.id} style={{ marginTop: '1rem' }}>
// <button onClick={() => signIn(provider.id)}>
// Sign in with {provider.name}
// </button>
// </div>
// );
})}
</div>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const providers = await getProviders();
return {
props: { providers },
};
}
Explanation of pages/auth/signin.tsx:
getProviders(): A NextAuth.js utility function that fetches all configured providers from your API route.
signIn(): The core function to initiate the sign-in process. When using the credentials provider, you pass the provider ID ('credentials') and an object containing the credentials.
redirect: false is important for handling errors on the client-side without a full page reload.
callbackUrl specifies where to redirect after successful authentication.
getServerSideProps: Used to fetch the available providers on the server-side before the page renders, ensuring they are available to the client.
Step 6: Wrap Your Application with SessionProvider
For NextAuth.js to work correctly across your application, you need to wrap your _app.tsx component with SessionProvider. This provides the session context to all components.
Modify pages/_app.tsx:
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { SessionProvider } from 'next-auth/react';
import type { Session } from 'next-auth'; // Import Session type
interface CustomAppProps extends AppProps {
pageProps: AppProps['pageProps'] & {
session?: Session; // Explicitly define session in pageProps
};
}
function MyApp({ Component, pageProps: { session, ...pageProps } }: CustomAppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default MyApp;
Explanation of pages/_app.tsx:
SessionProvider: A React Context Provider that makes the session data available to all child components.
session={session}: The session object is passed from pageProps, which is populated by NextAuth.js when a user is authenticated.
Step 7: Implement Authentication in a Page
Now, let's create a simple page to demonstrate how to check authentication status and sign in/out.
Modify pages/index.tsx:
// pages/index.tsx
import { useSession, signIn, signOut } from 'next-auth/react';
import Link from 'next/link';
export default function Home() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <p>Loading...</p>;
}
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<h1>Welcome to NextAuth.js Tutorial!</h1>
{session ? (
<div>
<p>Signed in as {session.user?.email || session.user?.name}</p>
{session.user?.id && <p>User ID: {session.user.id}</p>}
<button onClick={() => signOut()} style={{ padding: '0.5rem 1rem', marginTop: '1rem' }}>
Sign Out
</button>
</div>
) : (
<div>
<p>You are not signed in.</p>
<Link href="/auth/signin" passHref>
<button style={{ padding: '0.5rem 1rem', marginTop: '1rem' }}>
Sign In
</button>
</Link>
</div>
)}
<div style={{ marginTop: '2rem' }}>
<h2>Protected Content (Example)</h2>
{session ? (
<p>This content is only visible to authenticated users!</p>
) : (
<p>Please sign in to view this content.</p>
)}
</div>
</div>
);
}
Explanation of pages/index.tsx:
useSession(): A React Hook provided by NextAuth.js that gives you access to the session data (session) and the loading status (status).
session object: Contains information about the authenticated user (e.g., user.name, user.email). We also added user.id through our callbacks in [...nextauth].ts.
signIn() / signOut(): Functions to programmatically sign in or out a user. When used without arguments, signIn() will redirect to the signIn page defined in your NextAuth.js config.
Step 8: Run Your Application
Start your Next.js development server:
npm run dev
# or
yarn dev
Now, open your browser and navigate to http://localhost:3000
.
You should see the "You are not signed in" message.
Click the "Sign In" button, which will redirect you to http://localhost:3000/auth/signin.
Enter username: user and password: password and click "Sign In".
You should be redirected back to the home page, now showing "Signed in as J Smith" (or your hardcoded user's name/email).
Try signing out and signing in again.
Conclusion
Congratulations! You've successfully implemented a basic authentication system in your Next.js application using NextAuth.js with a credentials provider.
This tutorial provides a solid foundation. From here, you can explore:
Adding more providers: Integrate with Google, GitHub, Facebook, etc., by adding their respective providers to pages/api/auth/[...nextauth].ts.
Database Adapters: For production applications, you'll need a database to persist user data and sessions. NextAuth.js offers various adapters (Prisma, Mongoose, TypeORM, etc.).
Protecting API Routes: Use getServerSession (or getToken if not using session) in your API routes to protect them.
Customizing UI: Style your sign-in and other authentication pages to match your brand.
Role-Based Access Control (RBAC): Extend the session and token to include user roles for fine-grained access control.
NextAuth.js streamlines the complexities of authentication, allowing you to focus on building amazing applications. Happy coding!
Top comments (0)