DEV Community

Cover image for Next.js OAuth with NextAuth.js
Koji Mochizuki
Koji Mochizuki

Posted on • Updated on • Originally published at kjmczk.dev

Next.js OAuth with NextAuth.js

NextAuth.js is a library to easily and safely implement OAuth and email / passwordless authentication in Next.js apps. NextAuth.js v2 was released in June 2020, and currently v3 is under development. Frequent improvements are evidence of a good product.

Let's try NextAuth.js v3!

What we will:

  • Create a new Next.js app
  • Use NextAuth.js with SQLite
  • Sign in with Google

Create a Next.js App

Create a new Next.js app using create-next-app, which is named “next-auth-app” in this tutorial:

yarn create next-app next-auth-app
Enter fullscreen mode Exit fullscreen mode

Start the dev server and see the result on your browser:

cd next-auth-app
yarn dev
Enter fullscreen mode Exit fullscreen mode

Then, we'll build a simple foundation for implementing authentication.

Overwrite the index.js file in the pages directory as follows:

// pages/index.js

import Layout from '../components/layout';

const Home = () => (
  <Layout>
    <h1>Next Auth App</h1>
    <p>
      This is a sample project that uses{' '}
      <a href={`https://github.com/iaincollins/next-auth`}>NextAuth.js</a> to
      add authentication to <a href={`https://nextjs.org/`}>Next.js</a>.
    </p>
    <p>
      See <a href={`https://next-auth.js.org/`}>next-auth.js.org</a> for more
      information and documentation.
    </p>
  </Layout>
);

export default Home;
Enter fullscreen mode Exit fullscreen mode

Create a Layout component which will be common across all pages:

// components/layout.js

import Head from 'next/head';
import Header from '../components/header';

const Layout = ({ children }) => (
  <>
    <Head>
      <title>Next Auth App</title>
      <link rel="icon" href="/favicon.ico" />
    </Head>

    <Header />

    <main className="container">{children}</main>

    <style jsx global>{`
      *,
      *::before,
      *::after {
        box-sizing: border-box;
      }
      body {
        margin: 0;
        color: #333;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
          'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji',
          'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
      }
      .container {
        max-width: 42rem;
        margin: 0 auto;
        padding: 2rem 1.25rem;
      }
    `}</style>
  </>
);

export default Layout;
Enter fullscreen mode Exit fullscreen mode

Create a Header component:

// components/header.js

import Link from 'next/link';

const Header = () => {
  return (
    <header>
      <nav>
        <Link href="/">
          <a className="logo">
            <span style={{ color: '#f06292' }}>N</span>
            <span style={{ color: '#29b6f6' }}>A</span>
            <span style={{ color: '#8bc34a' }}>A</span>
          </a>
        </Link>

        <p>
          <button className="signInButton">Sign in</button>
        </p>
      </nav>

      <style jsx>{`
        header {
          border-bottom: 1px solid #ccc;
        }

        nav {
          display: flex;
          justify-content: space-between;
          align-items: center;
          max-width: 42rem;
          margin: 0 auto;
          padding: 0.2rem 1.25rem;
        }

        .logo {
          text-decoration: none;
          font-size: 1.5rem;
          font-weight: 600;
        }

        .signInButton {
          background-color: #1eb1fc;
          color: #fff;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          font-size: 1rem;
          padding: 0.5rem 1rem;
        }

        .signInButton:hover {
          background-color: #1b9fe2;
        }
      `}</style>
    </header>
  );
};

export default Header;
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the page:

Next Auth App

Use NextAuth.js

Let's add authentication with NextAuth.js to the Next.js app.

Configuration

First, install next-auth:

yarn add next-auth
Enter fullscreen mode Exit fullscreen mode

Next, add API route:

  • Create a directory called auth in pages/api.
  • Create a file called [...nextauth].js in pages/api/auth.
// pages/api/auth/[...nextauth].js

import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';

export default NextAuth({
  // Configure one or more authentication providers
  providers: [
    Providers.Google({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
    Providers.Facebook({
      clientId: process.env.FACEBOOK_ID,
      clientSecret: process.env.FACEBOOK_SECRET,
    }),
    Providers.Twitter({
      clientId: process.env.TWITTER_ID,
      clientSecret: process.env.TWITTER_SECRET,
    }),
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    Providers.Email({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
    }),
  ],

  // A database is optional, but required to persist accounts in a database
  database: process.env.DATABASE_URL,
});
Enter fullscreen mode Exit fullscreen mode

Then, create a top-level file called .env.local and set up environment variables:

NEXTAUTH_URL=http://localhost:3000
GOOGLE_ID=<YOUR GOOGLE ID>
GOOGLE_SECRET=<YOUR GOOGLE SECRET>
FACEBOOK_ID=
FACEBOOK_SECRET=
TWITTER_ID=
TWITTER_SECRET=
GITHUB_ID=
GITHUB_SECRET=
EMAIL_SERVER=smtp://username:password@smtp.example.com:587
EMAIL_FROM=NextAuth <noreply@example.com>
DATABASE_URL=sqlite://localhost/:memory:?synchronize=true
Enter fullscreen mode Exit fullscreen mode

In this tutorial, we'll sign in with Google as an example. Follow Setting up OAuth 2.0 to create an OAuth 2.0 client ID. The callback URL is set as {server}/api/auth/callback/{provider}. For Google OAuth, use http://localhost:3000/api/auth/callback/google.

If you use email sign-in, set EMAIL_SERVER and EMAIL_FROM correctly. For more information on configuring the SMTP server, check out Email in the NextAuth.js documentation.

If you want to use Gmail to send emails, see Using Gmail - Nodemailer.

In addition, specifying a database is required to use email sign-in and persist user data. For SQLite, set sqlite://localhost/:memory:?synchronize=true as above.

Install sqlite3:

yarn add sqlite3
Enter fullscreen mode Exit fullscreen mode

Add Hook

Add the useSession() hook provided by NextAuth.js to components/header.js, and update it as follows:

// components/header.js

import Link from 'next/link';
import { signIn, signOut, useSession } from 'next-auth/client';

const Header = () => {
  const [session, loading] = useSession();

  return (
    <header>
      <nav>
        <Link href="/">
          <a className="logo">
            <span style={{ color: '#f06292' }}>N</span>
            <span style={{ color: '#29b6f6' }}>A</span>
            <span style={{ color: '#8bc34a' }}>A</span>
          </a>
        </Link>

        <p>
          {!session && (
            <a
              href="/api/auth/signin"
              onClick={(e) => {
                e.preventDefault();
                signIn();
              }}
            >
              <button className="signInButton">Sign in</button>
            </a>
          )}
          {session && (
            <>
              <Link href="/profile">
                <a>
                  <span
                    style={{ backgroundImage: `url(${session.user.image})` }}
                    className="avatar"
                  />
                </a>
              </Link>
              <span className="email">{session.user.email}</span>
              <a
                href="/api/auth/signout"
                onClick={(e) => {
                  e.preventDefault();
                  signOut();
                }}
              >
                <button className="signOutButton">Sign out</button>
              </a>
            </>
          )}
        </p>
      </nav>

      <style jsx>{`
        header {
          border-bottom: 1px solid #ccc;
        }

        nav {
          display: flex;
          justify-content: space-between;
          align-items: center;
          max-width: 42rem;
          margin: 0 auto;
          padding: 0.2rem 1.25rem;
        }

        .logo {
          text-decoration: none;
          font-size: 1.5rem;
          font-weight: 600;
        }

        .avatar {
          border-radius: 2rem;
          float: left;
          height: 2.2rem;
          width: 2.2rem;
          background-color: white;
          background-size: cover;
          border: 2px solid #ddd;
        }

        .email {
          margin-right: 1rem;
          margin-left: 0.25rem;
          font-weight: 600;
        }

        .signInButton,
        .signOutButton {
          color: #fff;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          font-size: 1rem;
          padding: 0.5rem 1rem;
        }

        .signInButton {
          background-color: #1eb1fc;
        }
        .signInButton:hover {
          background-color: #1b9fe2;
        }

        .signOutButton {
          background-color: #333;
        }
        .signOutButton:hover {
          background-color: #555;
        }
      `}</style>
    </header>
  );
};

export default Header;
Enter fullscreen mode Exit fullscreen mode

The display switches depending on whether the user is signed in or not.

The signIn() and signOut() methods are not required, but they allow the user to return to the page where they started after completing each flow.

Also create a protected page for the signed-in user:

// pages/profile.js

import { useSession } from 'next-auth/client';
import Layout from '../components/layout';

const Profile = () => {
  const [session, loading] = useSession();

  if (loading) return <div>loading...</div>;
  if (!session) return <div>no session</div>;

  return (
    <Layout>
      {session && (
        <>
          <img src={session.user.image} className="avatar" />
          <h1>{session.user.name}</h1>
        </>
      )}

      <style jsx>{`
        .avatar {
          width: 220px;
          border-radius: 10px;
        }
      `}</style>
    </Layout>
  );
};

export default Profile;
Enter fullscreen mode Exit fullscreen mode

Add Provider

Using the supplied React <Provider> allows instances of useSession() to share the session object across components, by using React Context under the hood.

This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using pages/_app.js.

As noted in the NextAuth.js documentation. Follow this to create the _app.js page and add the <Provider>:

// pages/_app.js

import { Provider } from 'next-auth/client';

const App = ({ Component, pageProps }) => {
  const { session } = pageProps;

  return (
    <Provider session={session}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

That's it!

Sign In with Google

Now, let's sign in with Google OAuth.

First, restart the dev server to load the environment variables. Then, go to http://localhost:3000/ and click the Sign in button:

Sign In with OAuth or Email

Try signing in with Google. After successful sign-in, you should see:

Signed In with Google

Click the avatar icon to go to the profile page:

Profile Page

Awesome! We were able to implement authentication with just a few lines of code.

What else to check:

  • Sign out
  • Sign in with email

By the way, if you try to sign in with Google (e.g. example@gmail.com) after signing in with email (e.g. example@gmail.com) and creating an account, the result will be as follows:

Sign In Failed

Conversely, if you sign in with Google first, and then sign in with the same email address next time, you will be signed in with Google. This will not create duplicate accounts. Nice!

Conclusion

Implementing authentication is one of the most difficult tasks, especially for beginners in programming. In particular, you must pay close attention to security. NextAuth.js is security focused and super easy to use. It's really worth it!

You can find the code for this tutorial on GitHub.

Discussion (2)

Collapse
ramosdiego profile image
Diego

Let's say I want to create a blog app where people can post, edit and delete their blogs. Would I have to save their email alongside the blog post in a database and then authorize actions by checking whether the email in the database matches with session.user.email? In other words, the email becomes the only unique identifier I can use with this strategy?

Collapse
kjmczk profile image
Koji Mochizuki Author

You can use user id instead. Just add the callbacks option into [...nextauth].js:

callbacks: {
  session: async (session, user) => {
    session.user.id = user.id;
    return Promise.resolve(session);
  },
},