Mocking NextAuth.js in Storybook

A comprehensive guide to authenticated components

November 1, 2024

By Sohrab Saboori

Implementing NextAuth.js in Storybook for Secure Component Testing

In modern web development, authentication is a critical feature for many applications. NextAuth.js provides an easy and secure way to implement authentication in Next.js apps. Meanwhile, Storybook is an excellent tool for building UI components in isolation. However, integrating NextAuth.js with Storybook can be challenging due to context dependencies like SessionProvider.

In this guide, we'll walk through setting up a Next.js application with NextAuth.js and configuring Storybook to work seamlessly with components that rely on authentication. We'll address common issues, such as the useSession hook error in Storybook, and demonstrate how to handle different authentication scenarios. While we'll use credentials-based authentication for demonstration purposes, the strategies and solutions provided are applicable to any authentication provider supported by NextAuth.js.

1. Setting Up Next.js with NextAuth.js and Credentials Provider

1.1. Initialize a Next.js Project

Start by creating a new Next.js application:

npx create-next-app next-auth-storybook-example
cd next-auth-storybook-example

1.2. Install Dependencies

Install next-auth and other required dependencies:

npm install next-auth axios

1.3. Configure NextAuth.js with Credentials Provider

We'll use the CredentialsProvider to authenticate users via email and password. We'll simulate an authentication service by querying the JSON Placeholder API.

  • Create [...nextauth].ts in pages/api/auth/:
// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import axios from "axios";

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "text" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        try {
          const res = await axios.get(
            "https://jsonplaceholder.typicode.com/users?email=" +
              credentials?.email
          );
          const users = res.data;
          const user = users[0];

          if (user) {
            return { id: user.id, email: user.email, name: user.name };
          } else {
            return null;
          }
        } catch (error) {
          console.error(error);
          return null;
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.user = user;
      }
      return token;
    },
    async session({ session, token }) {
      session.user = token.user as any;
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/auth/signin",
    signOut: "/auth/signout",
    error: "/auth/error",
  },
});

1.4. Set Up Environment Variables

Create a .env.local file in the root directory:

# .env.local
NEXTAUTH_SECRET=your_nextauth_secret
NEXTAUTH_URL=http://localhost:3000

Generate a secure secret for NEXTAUTH_SECRET: You can quickly generate a 32-character base64 secret using the online tool provided by Vercel:

  • Visit https://generate-secret.vercel.app/32
  • The website will display a secure, randomly generated secret.
  • Copy the generated secret and paste it into your .env.local file as the value for NEXTAUTH_SECRET.
NEXTAUTH_SECRET=fhjK9n8jKj3h9Kj8hKj8hKj8hKj9h8Kj8

If you prefer to generate the secret locally, you can use OpenSSL:

openssl rand -base64 32

1.5. Create Sign-In Page

Create a custom sign-in page at pages/auth/signin.tsx:

// pages/auth/signin.tsx
import { signIn } from "next-auth/react";

export default function SignIn() {
  const handleSubmit = async (e: any) => {
    e.preventDefault();
    const email = e.target.email.value;
    const password = e.target.password.value;

    await signIn("credentials", {
      email,
      password,
      callbackUrl: "/",
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h1>Sign In</h1>
      <label>
        Email:
        <input name="email" type="email" />
      </label>
      <br />
      <label>
        Password:
        <input name="password" type="password" />
      </label>
      <br />
      <button type="submit">Sign In</button>
    </form>
  );
}

1.6. Protecting Routes

To protect pages or components, use the useSession hook and conditionally render content:

// pages/protected.tsx
import { useSession, signIn } from "next-auth/react";

export default function ProtectedPage() {
  const { data: session, status } = useSession();

  if (status === "loading") return <p>Loading...</p>;

  if (!session) {
    signIn(); // Redirect to sign-in page
    return null;
  }

  return <p>Welcome, {session?.user?.name}!</p>;
}

2. Creating an Authenticated Component

Let's create a UserProfile component that displays user information:

// components/UserProfile.tsx
import { useSession, signIn, signOut } from "next-auth/react";

export default function UserProfile() {
  const { data: session, status } = useSession();

  if (status === "loading") return <p>Loading...</p>;

  if (!session) {
    return (
      <>
        <p>You are not logged in.</p>
        <button onClick={() => signIn()}>Sign In</button>
      </>
    );
  }

  return (
    <>
      <p>Welcome, {session?.user?.name}</p>
      <p>Email: {session?.user?.email}</p>
      <button onClick={() => signOut()}>Sign Out</button>
    </>
  );
}

Add this component to your homepage to test it:

// pages/index.tsx
import UserProfile from "../components/UserProfile";

export default function Home() {
  return (
    <div>
      <h1>NextAuth.js with Storybook Example</h1>
      <UserProfile />
    </div>
  );
}

Wrap your application with <SessionProvider> in _app.tsx

// pages/_app.tsx
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

export default MyApp;

2.1. Test the Application

Run your application:

npm run dev

Visit http://localhost:3000 and navigate to the sign-in page. Use any email address to sign in since we're using a placeholder API.

To test the authentication flow, you can use any of the emails provided by the JSON Placeholder API. You can find more details about these users at the JSONPlaceholder Users API.

NextAuth.js Storybook page showing logged-out status with a 'Sign In' button.

NextAuth.js Sign In page with fields for email and password.

NextAuth.js Storybook page displaying logged-in user details and a 'Sign Out' button.

3. Installing and Configuring Storybook

3.1. Install Storybook

Initialize Storybook in your project:

npx sb init

3.2. Install TypeScript and Storybook Dependencies

If you're using TypeScript, ensure you have the necessary dependencies:

npm install --save-dev typescript @types/react @types/node

3.3. Create a Story for UserProfile

Create UserProfile.stories.tsx:

// components/UserProfile.stories.tsx
import { Meta, Story } from "@storybook/react";
import UserProfile from "./UserProfile";

export default {
  title: "Components/UserProfile",
  component: UserProfile,
} as Meta;

const Template: Story = (args) => <UserProfile {...args} />;

export const Default = Template.bind({});

3.4. Run Storybook

Start Storybook:

npm run storybook

4. Integrating NextAuth.js with Storybook

4.1. The useSession Hook Error

You'll encounter the following error in Storybook:

Error message in Storybook: 'useSession' must be wrapped in a SessionProvider.

4.2. Wrapping Stories with SessionProvider

To resolve this, wrap your stories with SessionProvider.

In your UserProfile.stories.tsx, import SessionProvider and Add a decorator to wrap your component:

export default {
  title: "Components/UserProfile",
  component: UserProfile,
  decorators: [
    (Story) => (
      <SessionProvider session={null}>
        <Story />
      </SessionProvider>
    ),
  ],
} as Meta;

Now, the SessionProvider wraps your component, providing the necessary context.

4.3. Handling TypeScript Issues

If TypeScript complains about types, ensure you import types correctly:

import { Meta, StoryFn } from "@storybook/react";

And define your template accordingly:

const Template: StoryFn = (args) => <UserProfile {...args} />;

5. Handling Different Authentication Scenarios

To simulate different authentication states in Storybook, you can provide mock session data.

5.1. Mocking Session Data

Create mock sessions:

const loggedOutSession = null;

const loggedInSession = {
  user: {
    name: "John Doe",
    email: "[email protected]",
    image: "https://via.placeholder.com/150",
  },
  expires: "9999-12-31T23:59:59.999Z",
};

5.2. Updating the Decorator to Use Args

Modify the decorator to use the session from args:

export default {
  title: "Components/UserProfile",
  component: UserProfile,
  decorators: [
    (Story, context) => (
      <SessionProvider session={context.args.session}>
        <Story />
      </SessionProvider>
    ),
  ],
} as Meta;

5.3. Creating Stories for Different States

Define stories for logged-in and logged-out states:

export const LoggedOut = Template.bind({});
LoggedOut.args = {
  session: loggedOutSession,
};

export const LoggedIn = Template.bind({});
LoggedIn.args = {
  session: loggedInSession,
};

Run Storybook:

Storybook sidebar with UserProfile component selected, displaying logged-in state with user information and 'Sign Out' button.

Storybook sidebar with UserProfile component selected, showing logged-out state with 'Sign In' button.

Final Thoughts on Mocking NextAuth.js in Storybook

Integrating authentication into your application is essential but often challenging when testing components in isolation. By mocking NextAuth.js in Storybook, you can develop and test authenticated components efficiently without relying on a live authentication flow or backend services. This approach enhances your development workflow, improves component isolation, and ensures your UI behaves correctly under different authentication states.

In this guide, we've demonstrated how to set up NextAuth.js with the CredentialsProvider in a Next.js application and integrate it with Storybook. By addressing common issues like the useSession hook error and showing how to wrap your components with the SessionProvider and mock session data, you can apply these techniques to any authentication provider supported by NextAuth.js, making your components more robust and your development process more efficient.

You can find the complete source code for this example on GitHub:

https://github.com/rikaweb/next-auth-storybook-example

References

Photo of Fishtank employee Sohrab Saboori

Sohrab Saboori

Senior Full-Stack Developer

Sohrab is a Senior Front-End Developer with extensive experience in React, Next.js, JavaScript, and TypeScript. Sohrab is committed to delivering outstanding digital solutions that not only meet but exceed clients' expectations. His expertise in building scalable and efficient web applications, responsive websites, and e-commerce platforms is unparalleled. Sohrab has a keen eye for detail and a passion for creating seamless user experiences. He is a problem-solver at heart and enjoys working with clients to find innovative solutions to their digital needs. When he's not coding, you can find him lifting weights at the gym, pounding the pavement on the run, exploring the great outdoors, or trying new restaurants and cuisines. Sohrab believes in a healthy and balanced lifestyle and finds that these activities help fuel his creativity and problem-solving skills.