Simple Authentication For Next.js & React With Clerk
Jul 10, 2023Authentication 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,
},
Header
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 thesignUp
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.