Simple Authentication For Next.js & React With Clerk

next.js react Jul 10, 2023

Authentication and user management can be a real pain in the neck. Especially when you are dealing with front-end frameworks like React. There are many different ways to implement authentication and a lot of different services that you can use. In this article, we are going to look at setting up not only authentication, but complete user management with a tool called Clerk. Clerk can be used with a React SPA, Next.js, Remix and more and can be easily integrated with databases like Firebase and Supabase. You can also easily integrate it with your own custom backend user authorization.

In this article, we will be using Next.js and we will setup complete user management from scratch using Clerk.

Here are the links to the Clerk documentation. You can use them as a supplement to this article:

Next.js Setup

Let's start by setting up a brand new Next.js project. We can do this by running the following command:

npx create-next-app@latest

I am going to choose all of the defaults. I am not using TypeScript. I am not using an src folder and I am using the app directory/layout. I am also going to choose to use Tailwind CSS for styling. You can choose whatever you like.

Let's just clear out the page.js file. It should look like this:

export default function Home( ) {
  return (
    <>
      <h1>Home</h1>
    </>
  );
}

I also prefer to use the jsx extension, so I am going to change page.js to page.jsx. I am also going to change the layout.js to layout.jsx.

Styles

I am going to clear out all off the default styles. Open the globals.css file and remove everything except the Tailwind imports. It should look like this:

@tailwind base;
@tailwind components;
@tailwind utilities;

Installation

Now we can install Clerk. We can do this by running the following command:

npm install @clerk/nextjs

If you were using something like Create React App, you would install @clerk/react instead.

Clerk Setup & Environment Keys

You need to sign up at Clerk and create a new project. Once you have created a project, you will be given a Clerk Frontend API Key. You will need to add this to your .env.local file. They will look something like this:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YOU
CLERK_SECRET_KEY=sk_test_OvHiJQyHNMBuV0z0p8d6Z5ghsN8wuC30rL6hYEHYnC

Login Methods

You can choose how you want users to login by going to User & Authentication -> Email, Phone, Username. I will use Email and Password with an email verification. You can also choose to have users login using only an email verification link.

I will also choose to collect the user's name.

You can choose the social login methods that you want to be used by going to User & Authentication -> Social Connections. I am going to choose Google and GitHub. You can choose whatever you like.

Clerk Provider

We have to wrap our application with the Clerk Provider. Open the layout.js, bring in the ClerkProvider and wrap the Component with it. It should look like this:

import './globals.css';
import { Inter } from 'next/font/google';
import { ClerkProvider } from '@clerk/nextjs';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
  title: 'Clerk App',
  description: 'Example Clerk App',
};

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      <html lang='en'>
        <body className={inter.className}>
          <Header />
          <main className='container mx-auto'>
            <div className='flex items-start justify-center min-h-screen'>
              <div className='mt-20'>{children}</div>
            </div>
          </main>
        </body>
      </html>
    </ClerkProvider>
  );
}

I also added a container class and some classes to center the content.

I want the container to always set to the middle without having to add the mx-auto class to every element, so open the tailwind.config.js file and add the following code in the theme object:

container: {
  center: true,
},

Let's create a very simple header component. Create a folder called components in the app folder. Inside of that folder, create a file called header.jsx. Add the following code to the file:

import Link from 'next/link';

const Header = ({ username }) => {
  return (
    <nav className='bg-blue-700 py-4 px-6 flex items-center justify-between mb-5'>
      <div className='flex items-center'>
        <Link href='/'>
          <div className='text-lg uppercase font-bold text-white'>
            Clerk App
          </div>
        </Link>
      </div>
      <div className='text-white'>
        <Link href='sign-in' className='text-gray-300 hover:text-white mr-4'>
          Sign In
        </Link>
        <Link href='sign-up' className='text-gray-300 hover:text-white mr-4'>
          Sign Up
        </Link>
      </div>
    </nav>
  );
};

export default Header;

Right now, it shows the auth links, but once we are authenticated, we will show the username and a logout link. We will do that in a bit.

Protecting Pages

Usually, when you implement authentication, you have certain areas/pages that you want to protect. We are going to do that now by creating a file called middleware.js in the root folder.

Add the following code to the file:

import { authMiddleware } from '@clerk/nextjs';

export default authMiddleware();

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

With this, your entire app and all pages are protected except anything with /api or /trpc, which is for TypeScript typesafe APIs. Everything else is protected. If you reload your page, you will see a login screen.

There are many ways to go about this, but what I want to do is make all pages private by default and then make certain pages public. We can do this by adding the following code to the middleware.js file:

import { authMiddleware } from '@clerk/nextjs';

export default authMiddleware({
  publicRoutes: ['/'],
});

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Now your homepage should be accessible again. If you go to any other page, you will see a login screen.

Clerk Branding

When using the free account, Clerk branding will be shown on the login screen. You can remove this by upgrading your account. You can do this by going to https://clerk.com/pricing. For learning and testing, this is fine, but in a production app, you will probably want to upgrade.

Building Your Own Sign In/Out Pages

You don't have to use the pre-made sign in/out pages that Clerk provides. You can build your own and use Clerk components. You also do not have to add them to the auth middleware to make them public. You can do this by adding the following code to your .env file.

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

You can change the url to whatever you would like, for instance, if you want to use ./login instead of ./sign-in, you can do that. You can also change the redirect url after sign in and sign up.

Catch All Routes

We are going to use the Next.js catch all route to handle all of our pages. Create a folder in the app directory called sign-up. Inside of that create another folder called [[...sign-up]]. Then create a file in that folder called page.jsx. Now we have a catch-all route for all sign-up pages.

Add the following code to the file:

import { SignUp } from '@clerk/nextjs';

const SignUpPage = () => {
  return (
    <>
      <SignUp />
    </>
  );
};
export default SignUpPage;

Let's do the same for sign in. Create a folder called sign-in, another folder in that one called [[...sign-in]] and add a file named page.jsx.

Add the following code to the file:

import { SignIn } from '@clerk/nextjs';

const SignInPage = () => {
  return (
    <>
      <SignIn />
    </>
  );
};
export default SignInPage;

Dashboard

let's create a dashboard page. Create a folder called dashboard in the app folder. Inside of that folder, create a file called page.jsx. Add the following code to the file:

const Dashboard = () => {
  return (
    <>
      <h1 className='text-2xl font-bold mb-5'>Dashboard</h1>
      <p className='mb-5'>Welcome to the dashboard!</p>
    </>
  );
};

export default Dashboard;

You should now see your custom sign in and sign up pages.

Log In

Let's try creating an account. I am going to choose to create an account using Google. Once I authenticate, I am redirected to the dashboard. If you go to the Clerk dashboard, you will see that the user has been created.

UserButton

The <UserButton /> component is a component that you can use to show the user's name and a logout button. Let's add this to our header. Open the Header.jsx file and add the following code:

import Link from 'next/link';
import { UserButton } from '@clerk/nextjs';

const Header = ({ username }) => {
  return (
    <nav className='bg-blue-700 py-4 px-6 flex items-center justify-between mb-5'>
      <div className='flex items-center'>
        <Link href='/'>
          <div className='text-lg uppercase font-bold text-white'>
            Clerk App
          </div>
        </Link>
      </div>
      <div className='text-white flex items-center'>
        <Link href='sign-in' className='text-gray-300 hover:text-white mr-4'>
          Sign In
        </Link>
        <Link href='sign-up' className='text-gray-300 hover:text-white mr-4'>
          Sign Up
        </Link>
        <div className='ml-auto'>
          <UserButton afterSignOutUrl='/' />
        </div>
      </div>
    </nav>
  );
};

export default Header;

Now you can see an avatar with a dropdown with a logout link and a link to show the settings. I also added a redirect url for sign out to go to the homepage.

Account Settings

You can access your account settings from here as well. This includes email addresses, connected accounts, the ability to delete your account and more.

I think for the tiny amount of code that we have written, this is amazing.

Email Login

Before we do anything else, let's logout and then register with an email and password.

Click on 'Sign Up' And fill out the form. You will be redirected to a form to input a code.

You should have received an email with a code.

Enter the code and you will be logged in and redirected to the dashboard.

Conditional Rendering

Right now, we can still see the sign in and sign up links. Let's change that. There are a few ways that we can do this.

auth()

We can bring in auth function from @clerk/nextjs and destructure the user id from it. We can then use that to conditionally render the sign in and sign up links.

Open the Header.jsx file and add the following code:

import Link from 'next/link';
import { UserButton, auth } from '@clerk/nextjs';

const Header = ({ username }) => {
  const { userId } = auth();

  return (
    <nav className='bg-blue-700 py-4 px-6 flex items-center justify-between mb-5'>
      <div className='flex items-center'>
        <Link href='/'>
          <div className='text-lg uppercase font-bold text-white'>
            Clerk App
          </div>
        </Link>
      </div>

      <div className='text-white flex items-center'>
        {!userId && (
          <>
            <Link
              href='sign-in'
              className='text-gray-300 hover:text-white mr-4'
            >
              Sign In
            </Link>
            <Link
              href='sign-up'
              className='text-gray-300 hover:text-white mr-4'
            >
              Sign Up
            </Link>
          </>
        )}
        <div className='ml-auto'>
          <UserButton afterSignOutUrl='/' />
        </div>
      </div>
    </nav>
  );
};

export default Header;

The <UserButton /> will always only show if we are logged in, so we don't have to worry about that.

currentUser

We can also bring in currentUser from @clerk/nextjs. Let's do that and see what it gives us:

import Link from 'next/link';
// Add currentUser
import { UserButton, auth, currentUser } from '@clerk/nextjs';

const Header = async ({ username }) => {
  const { userId } = auth();

  // Add these lines
  const user = await currentUser();
  console.log(user);

  return (
    <nav className='flex items-center justify-between px-6 py-4 mb-5 bg-blue-700'>
      <div className='flex items-center'>
        <Link href='/'>
          <div className='text-lg font-bold text-white uppercase'>
            Clerk App
          </div>
        </Link>
      </div>
      <div className='flex items-center text-white'>
        {!userId && (
          <>
            <Link
              href='sign-in'
              className='text-gray-300 hover:text-white mr-4'
            >
              Sign In
            </Link>
            <Link
              href='sign-up'
              className='text-gray-300 hover:text-white mr-4'
            >
              Sign Up
            </Link>
          </>
        )}
        <div className='ml-auto'>
          <UserButton afterSignOutUrl='/' />
        </div>
      </div>
    </nav>
  );
};

export default Header;

Depending on how you log in, you will see different things. Just be aware of the structure of the object for all login methods before using it in your UI.

useAuth Hook

You don't have to do this now, but for client side components, you can use the useAuth hook. It would look something like this:

import { useAuth } from '@clerk/nextjs';

export default function Example( ) {
  const { isLoaded, userId, sessionId, getToken } = useAuth();

  // In case the user signs out while on the page.
  if (!isLoaded || !userId) {
    return null;
  }

  return (
    <div>
      Hello, {userId} your current active session is {sessionId}
    </div>
  );
}

<UserProfile /> Component

If you want to embed the profile and settings on the page, you can use this component. Let's create a new folder called profile and a page.jsx inside of it. Add the following code to the file:

import { UserProfile } from '@clerk/nextjs';

const ProfilePage = () => {
  return (
    <>
      <UserProfile />
    </>
  );
};

export default ProfilePage;

Add it to the public pages in the middleware.js file.

export default authMiddleware({
  publicRoutes: ['/', '/profile'],
});

Now add a link in the Header.jsx file to show only when logged in.

{
  userId && (
    <Link href='profile' className='text-gray-300 hover:text-white mr-4'>
      Profile
    </Link>
  );
}

When you go to the profile page, you should see something like this:

Themes

You can customize the appearance of Clerk components by using themes. They offer a set of themes that can be used with the appearance prop. You can install the themes package with:

npm i @clerk/themes

Now, go into your layout.jsx file and import the theme:

import { dark } from '@clerk/themes';

Then add it to the <ClerkProvider /> component:

<ClerkProvider
  appearance={{
    baseTheme: dark,
  }}
>

Now you can see that the appearance has changed.

I'll change it back to the light theme.

import { light } from '@clerk/themes';

<ClerkProvider
  appearance={{
    baseTheme: light,
  }}
>

Custom Components

Even though you have the ability to change the appearance of the components, you may still want to build completely custom components and a custom workflow. You can do this by using the 'useSignIn' and 'useSignUp' hooks.

We are going to create a completely custom sign up page so that you can see how this works.

Create a new folder called register and a file called page.jsx inside of it. Add the following code:

'use client';
import { useState } from 'react';
import { useSignUp } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';

const RegisterPage = () => {
  const { isLoaded, signUp, setActive } = useSignUp();
  const [email, setEmail] = useState('');
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [password, setPassword] = useState('');
  const [pendingVerification, setPendingVerification] = useState(false);
  const [code, setCode] = useState('');
  const router = useRouter();

  // Form Submit
  const handleSubmit = async (e) => {};

  // Verify User Email Code
  const onPressVerify = async (e) => {};

  return (
    <div className='border p-5 rounded' style={{ width: '500px' }}>
      <h1 className='text-2xl mb-4'>Register</h1>
      {!pendingVerification && (
        <form onSubmit={handleSubmit} className='space-y-4 md:space-y-6'>
          <div>
            <label
              htmlFor='first_name'
              className='block mb-2 text-sm font-medium text-gray-900'
            >
              First Name
            </label>
            <input
              type='text'
              name='first_name'
              id='first_name'
              onChange={(e) => setFirstName(e.target.value)}
              className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5'
              required={true}
            />
          </div>
          <div>
            <label
              htmlFor='last_name'
              className='block mb-2 text-sm font-medium text-gray-900'
            >
              Last Name
            </label>
            <input
              type='text'
              name='last_name'
              id='last_name'
              onChange={(e) => setLastName(e.target.value)}
              className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5'
              required={true}
            />
          </div>
          <div>
            <label
              htmlFor='email'
              className='block mb-2 text-sm font-medium text-gray-900'
            >
              Email Address
            </label>
            <input
              type='email'
              name='email'
              id='email'
              onChange={(e) => setEmail(e.target.value)}
              className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5'
              placeholder='name@company.com'
              required={true}
            />
          </div>
          <div>
            <label
              htmlFor='password'
              className='block mb-2 text-sm font-medium text-gray-900'
            >
              Password
            </label>
            <input
              type='password'
              name='password'
              id='password'
              onChange={(e) => setPassword(e.target.value)}
              className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5'
              required={true}
            />
          </div>
          <button
            type='submit'
            className='w-full text-white bg-blue-600 hover:bg-blue-700 font-medium rounded-lg text-sm px-5 py-2.5 text-center'
          >
            Create an account
          </button>
        </form>
      )}
      {pendingVerification && (
        <div>
          <form className='space-y-4 md:space-y-6'>
            <input
              value={code}
              className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5'
              placeholder='Enter Verification Code...'
              onChange={(e) => setCode(e.target.value)}
            />
            <button
              type='submit'
              onClick={onPressVerify}
              className='w-full text-white bg-blue-600 hover:bg-blue-700 font-medium rounded-lg text-sm px-5 py-2.5 text-center'
            >
              Verify Email
            </button>
          </form>
        </div>
      )}
    </div>
  );
};

export default RegisterPage;

We created a register form and added a bunch of component state including the 3 values isLoaded, signUp, and setActive from the useSignUp hook.

  • isLoaded is a boolean that is true when the signUp object is ready to be used.
  • signUp is an object that contains all the methods to sign up a user.
  • setActive is a function that will set the current form to be active. This is useful if you want to show the sign in form after the user signs up.

Now add the /register as a public page in the middleware.js file:

export default authMiddleware({
  publicRoutes: ['/', '/register'],
});

Now go to the /register page and you'll see the custom form.

The way this will work is if the pendingVerification is true, it will show the verification code input. If it's false, it will show the sign up form. You can test by changing the pendingVerification to true in the code.

Form Submit

Now we will handle the form submission. Add the following to the handleSubmit function:

const handleSubmit = async (e) => {
  e.preventDefault();

  if (!isLoaded) {
    return;
  }

  try {
    await signUp.create({
      first_name: firstName,
      last_name: lastName,
      email_address: email,
      password,
    });

    // send the email.
    await signUp.prepareEmailAddressVerification({ strategy: 'email_code' });

    // change the UI to our pending section.
    setPendingVerification(true);
  } catch (err) {
    console.error(err);
  }
};

We are calling the create method on the signUp object and passing in the user data. Then we call prepareEmailAddressVerification and pass in the strategy. This will send the email to the user. Then we set pendingVerification to true to show the verification code input.

Verify Code

Now we need to handle the verification code. Add the following to the onPressVerify function:

const onPressVerify = async (e) => {
  e.preventDefault();
  if (!isLoaded) {
    return;
  }

  try {
    const completeSignUp = await signUp.attemptEmailAddressVerification({
      code,
    });
    if (completeSignUp.status !== 'complete') {
      /*  investigate the response, to see if there was an error
         or if the user needs to complete more steps.*/
      console.log(JSON.stringify(completeSignUp, null, 2));
    }
    if (completeSignUp.status === 'complete') {
      await setActive({ session: completeSignUp.createdSessionId });
      router.push('/');
    }
  } catch (err) {
    console.error(JSON.stringify(err, null, 2));
  }
};

We are calling the attemptEmailAddressVerification method on the signUp object and passing in the code. Then we check the status property of the response. If it's complete, we call setActive and pass in the createdSessionId and then redirect to the home page.

Now try it out. You should be able to submit the form, check your email for the code and then enter it to verify.

So you can see that you are not limited to Clerk's components. They are extremely useful but you can also create your own forms and use the methods from the useSignUp and useSignIn hook. I will leave it up to you to add the custom sign in form.

Conclusion

There you have it. We have complete authentication and user management in our app. You can also customize the look of things from within the Clerk dashboard. If you want to change to using a magic link instead of a code, you can do that as well. In my opinion, this is the easiest way to add authentication to your app and you get a ton of features.

You can find the repo for this project here

Stay connected with news and updates!

Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.

We hate SPAM. We will never sell your information, for any reason.