Jun 16, 2021
Aniket Bhattacharyea
In this article, you'll learn the pros and cons of developing or buying a user management system so you’re ready to make the right choice for your project.
Should you build a user management system for your application or offload the task to a third-party solution? Part one of this series discussed some of the advantages and disadvantages of both approaches and listed use cases where each one shines.
In this article, you'll learn the steps necessary to build a user management system from scratch in Next.js using Clerk.
At the beginning of the article, you'll build a user management system using the next-auth library. In the second half, you'll rebuild the same app using Clerk.
The goal of this article is not to give a detailed tutorial on how to build a complete user management system, but instead, you'll see how much work is involved in building one from scratch versus buying from a third party. To this end, the example used here is basic and minimal, and out of all the features listed in part one, only an email/password-based authentication and SSO (with Google) will be implemented.
The Next.js app will have two routes: the root /
route will be public, and the /profile
route will be protected and require the user to sign in.
First, create a Next.js app:
1npx create-next-app next-auth-demo2cd next-auth-demo
Install the next-auth
library:
1npm install next-auth
Write the following code in pages/index.js
for the /
route:
1import styles from "../styles/Home.module.css";2import Link from 'next/link';34export default function Home() {56return (7<div className={styles.container}>8<main className={styles.title}>9<h1>Welcome</h1>10<Link href="/profile">11<a>Go to your Profile</a>12</Link>13</main>14</div>15);16}
In pages/_app.js
, you need to wrap Component
with SessionProvider
from next-auth
:
1import '../styles/globals.css'2import { SessionProvider } from "next-auth/react"34function MyApp({ Component, pageProps: { session, ...pageProps } }) {5return (6<SessionProvider session={session}>7<Component {...pageProps} />8</SessionProvider>9)10}1112export default MyApp
Create pages/api/auth
directory and add a file [...nextauth].js
in the directory. This file will act as a catch-all route, and any route in /api/auth/*
will be handled by next-auth
.
1import NextAuth from "next-auth"2import CredentialsProvider from "next-auth/providers/credentials";3import { custom } from 'openid-client';45custom.setHttpOptionsDefaults({6timeout: 20000,7})89export default NextAuth({10providers: [11CredentialsProvider({12name: "Credentials",13credentials: {14email: { label: "Email", type: "email" },15password: { label: "Password", type: "password" }16},17async authorize(credentials, req) {1819const res = await fetch("http://localhost:3000/api/signin", {20method: 'POST',21body: JSON.stringify(credentials),22headers: { "Content-Type": "application/json" }23})2425if (res.ok) {26const user = await res.json();27return user;28} else {29return null;30}31}32}),33],34session: {35strategy: 'jwt'36},37secret: "SoVXiTrf/OdmwGlVUDxS9w44oowgkcM51e2P/ev8ry4="38})
In this instance, you use Credentials Provider to add email/password authentication. The authorize()
function dictates how the actual authentication is performed, and you can call your backend API to validate the credentials and fetch the user. The return value of authorize()
is stored in the JWT as the user object. A return value of null
indicates a failed authentication. The secret
is just a random thirty-two character key.
In this code, the route /api/signin
is used to authenticate the user. To do that, first you create the file pages/api/signin.js
:
1export default function handler(req, res) {2const { email, password } = req.body;3if (email === "abc@xyz.com" && password === "password") {4res.status(200).json({ id: 1, name: "J Smith", email: "abc@xyz.com" });5} else {6res.status(401).json({ message: "Invalid credentials" });7}8}
For demonstration purposes, this code uses a hard-coded user. In a real-world app, you'll likely have a database of users and a custom logic to fetch and validate users from this database.
Create the protected page in pages/profile.js
:
1import { useSession, signIn, signOut } from "next-auth/react"2import styles from "../styles/Home.module.css"34export default function Profile() {5const { data: session } = useSession()6if (session) {7return (8<div className={styles.title}>9Signed in as {session.user.email} <br />10<button onClick={() => signOut()}>Sign out</button>11</div>12)13}14return (15<div className={styles.title}>16Not signed in <br />17<button onClick={() => signIn()}>Sign in</button>18</div>19)20}
The code uses the useSession
hook to know if the user is logged in or not. If the user is logged in, the profile page is shown; otherwise, he is prompted to sign in. The signIn()
and signOut()
functions are provided by next-auth
to sign the user in and out.
Start the server with npm run dev
and visit http://localhost:3000
. You should see the following screen:
Clicking on Go to your Profile takes you to your profile page. You should see a Not signed in message.
Click on Sign in, and you'll be taken to the sign-in page created by next-auth
.
The "correct" email/password combo is "abc@xyz.com" and "password." Using these credentials, you should be able to log in.
next-auth
next-auth
supports a plethora of social providers and can also work with custom OAuth providers. In this tutorial, you'll use Google. First, you'll need to create a Google Developer account.
You can follow this documentation to set up OAuth 2.0 and create a new Client ID. Then, use http://localhost:3000/api/auth/callback/google
in the Authorized redirect URIs field.
Once the client ID is created, click on it, and copy the Client ID and Client secret.
Save these values as environment variables in your .env.local
file.
1GOOGLE_ID=<YOUR_CLIENT_ID>2GOOGLE_SECRET=<YOUR_CLIENT_SECRET>
Now, you'll need to add Google Provider in the providers
array in pages/api/auth/[...nextauth].js
:
1import GoogleProvider from "next-auth/providers/google"; // add this import2...34export default NextAuth({5providers: [6...7GoogleProvider({8clientId: process.env.GOOGLE_ID,9clientSecret: process.env.GOOGLE_SECRET,10})11],12...13})
Restart the server, and you should have a Sign in with Google button on the sign-in page (you might need to sign out if you haven't already). Using this, you can log in with your Google account.
Even though libraries like next-auth
make writing a user management system relatively easy, there are still complexities. You're not even using a database, and you still have to write a decent amount of code to set up a basic authentication system. You can certainly imagine the time and effort required to implement all the features mentioned in part one.
You can find the code for this app in the GitHub repo.
Now you're going to set up the same authentication system, this time using Clerk. Clerk is a powerful authentication tool that integrates with modern web development frameworks, including Next.js, and provides a complete user management system.
Visit the Clerk dashboard and create a new application. For now, make sure none of the social login providers are enabled. You'll enable them later when you add SSO.
Clerk will, by default, create a Development
instance of your application. From the application's homepage, copy the Frontend API Key and paste it into the .env.local
file in NEXT_PUBLIC_CLERK_FRONTEND_API
variable.
1NEXT_PUBLIC_CLERK_FRONTEND_API=<YOUR_FRONTEND_API_URL>
Install the @clerk/nextjs
package:
1npm install @clerk/nextjs
Then, in your pages/_app.js
file, you need to wrap the Component
with the ClerkProvider
component. Clerk provides SignedIn, SignedOut, and RedirectToSignIn components, which can be used to set up an authentication flow.
1import '../styles/globals.css';2import { ClerkProvider, SignedIn, SignedOut, RedirectToSignIn } from '@clerk/nextjs';3import { useRouter } from 'next/router';45const publicPages = ["/"];67const { pathname } = useRouter();89const isPublicPage = publicPages.includes(pathname);1011function MyApp({ Component, pageProps }) {12return (13<ClerkProvider>14{isPublicPage ? (15<Component {...pageProps} />16) : (17<>18<SignedIn>19<Component {...pageProps} />20</SignedIn>21<SignedOut>22<RedirectToSignIn redirectUrl={pathname} />23</SignedOut>24</>25)}26</ClerkProvider>27);28}2930export default MyApp;
The code first extracts the current path in the pathname
variable and matches it against the entries in the publicPages
array. If the path is a public page, Component
is rendered. If the path is not public, the SignedIn
component makes sure the page is only rendered if the user is signed in. If the user is not signed in, the SignedOut
component is rendered. Here, it uses the RedirectToSignIn
component to redirect the user to the sign-in page. The sign-in and sign-up pages will use Clerk's hosted pages, which means you don't need to design them by hand.
Change pages/profile.js
to the following code:
1import { useUser, UserButton } from "@clerk/nextjs";23export default function Profile() {45const { firstName } = useUser();67return (8<>9<style jsx>{`10.container {11max-width: 65rem;12margin: 1.5rem auto;13padding-left: 1rem;14padding-right: 1rem;15}16`}1718</style>19<div className="container">20<header>21{/* Mount the UserButton component */}22<UserButton />23</header>24<main>Hello, {firstName}!</main>25</div>26</>2728);29}
Above, the useUser
hook is used to extract the user's first name. The UserButton
component is also provided by Clerk and gives signed-in users a way to manage their accounts, as well as sign out.
Start the server with npm run dev
and again, visit the /profile
page. You should see Clerk's hosted sign-in page.
You can click on Sign up and register a new account. When you sign up, you'll receive a verification email, and upon verification, you'll be logged in.
After you have successfully signed in, you can see the UserButton
on the profile page.
Clicking on Manage account takes you to your profile management page, where you can modify your information, change your password, manage 2FA, and see your active devices.
In the application dashboard, go to Authentication > Social Login. Here, you can turn on any of the supported providers. In development, you don't need any credentials because you can use Clerk's shared credentials.
To start, go ahead and enable Google.
On the configuration page, enter your OAuth credentials, or you can leave it blank if you're planning on using the shared credentials.
If you supply your own credentials, make sure to update the Authorized redirect URIs in the Google console with the value shown in Clerk.
Save the settings, and you should now have a Sign in with Google button in your application.
As you can see, with considerably less code, you not only get a secure and solid authentication system with SSO and a profile management page, but you also get the added bonus of controlling the different aspects of the user management system from Clerk's dashboard, without having to change the code.
Whether you want to turn on magic links, use OTP, or add more social providers, Clerk makes it easy and efficient.
The code for this app is available in this GitHub repo.
This article demonstrated building a user management system from scratch, and then using Clerk to build the same. The rapid development speed and ease of use of Clerk make it an ideal solution for a user management system.
Start completely free for up to 5,000 monthly active users and up to 10 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.