DEV Community

Hiro Ventolero
Hiro Ventolero

Posted on

Seamless Authentication in Next.js: A Step-by-Step NextAuth.js Tutorial (TypeScript)

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Install NextAuth.js
Now, install the next-auth package:

npm install next-auth
# or
yarn add next-auth
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

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 },
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)