Back

Insights


Jun 16, 2021

Preview

Insights


Jun 16, 2021

Build or Buy? A Look at User Management with Next.js: Part 2

Aniket Bhattacharyea

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.

Building from Scratch

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:

1
npx create-next-app next-auth-demo
2
cd next-auth-demo

Install the next-auth library:

1
npm install next-auth

Write the following code in pages/index.js for the / route:

1
import styles from "../styles/Home.module.css";
2
import Link from 'next/link';
3
4
export default function Home() {
5
6
return (
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:

1
import '../styles/globals.css'
2
import { SessionProvider } from "next-auth/react"
3
4
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
5
return (
6
<SessionProvider session={session}>
7
<Component {...pageProps} />
8
</SessionProvider>
9
)
10
}
11
12
export 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.

1
import NextAuth from "next-auth"
2
import CredentialsProvider from "next-auth/providers/credentials";
3
import { custom } from 'openid-client';
4
5
custom.setHttpOptionsDefaults({
6
timeout: 20000,
7
})
8
9
export default NextAuth({
10
providers: [
11
CredentialsProvider({
12
name: "Credentials",
13
credentials: {
14
email: { label: "Email", type: "email" },
15
password: { label: "Password", type: "password" }
16
},
17
async authorize(credentials, req) {
18
19
const res = await fetch("http://localhost:3000/api/signin", {
20
method: 'POST',
21
body: JSON.stringify(credentials),
22
headers: { "Content-Type": "application/json" }
23
})
24
25
if (res.ok) {
26
const user = await res.json();
27
return user;
28
} else {
29
return null;
30
}
31
}
32
}),
33
],
34
session: {
35
strategy: 'jwt'
36
},
37
secret: "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:

1
export default function handler(req, res) {
2
const { email, password } = req.body;
3
if (email === "abc@xyz.com" && password === "password") {
4
res.status(200).json({ id: 1, name: "J Smith", email: "abc@xyz.com" });
5
} else {
6
res.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:

1
import { useSession, signIn, signOut } from "next-auth/react"
2
import styles from "../styles/Home.module.css"
3
4
export default function Profile() {
5
const { data: session } = useSession()
6
if (session) {
7
return (
8
<div className={styles.title}>
9
Signed in as {session.user.email} <br />
10
<button onClick={() => signOut()}>Sign out</button>
11
</div>
12
)
13
}
14
return (
15
<div className={styles.title}>
16
Not 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:

The index page

Clicking on Go to your Profile takes you to your profile page. You should see a Not signed in message.

The profile page

Click on Sign in, and you'll be taken to the sign-in page created by next-auth.

The sign-in page

The "correct" email/password combo is "abc@xyz.com" and "password." Using these credentials, you should be able to log in.

The profile page after the user has signed in

Adding SSO 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.

The credentials page in Google console

Save these values as environment variables in your .env.local file.

1
GOOGLE_ID=<YOUR_CLIENT_ID>
2
GOOGLE_SECRET=<YOUR_CLIENT_SECRET>

Now, you'll need to add Google Provider in the providers array in pages/api/auth/[...nextauth].js:

1
import GoogleProvider from "next-auth/providers/google"; // add this import
2
...
3
4
export default NextAuth({
5
providers: [
6
...
7
GoogleProvider({
8
clientId: process.env.GOOGLE_ID,
9
clientSecret: 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.

The sign-in page with SSO

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.

Building an Authentication System with Clerk

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.

The new application page in Clerk

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.

1
NEXT_PUBLIC_CLERK_FRONTEND_API=<YOUR_FRONTEND_API_URL>

The frontend API key in the Clerk dashboard

Install the @clerk/nextjs package:

1
npm 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.

1
import '../styles/globals.css';
2
import { ClerkProvider, SignedIn, SignedOut, RedirectToSignIn } from '@clerk/nextjs';
3
import { useRouter } from 'next/router';
4
5
const publicPages = ["/"];
6
7
const { pathname } = useRouter();
8
9
const isPublicPage = publicPages.includes(pathname);
10
11
function MyApp({ Component, pageProps }) {
12
return (
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
}
29
30
export 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:

1
import { useUser, UserButton } from "@clerk/nextjs";
2
3
export default function Profile() {
4
5
const { firstName } = useUser();
6
7
return (
8
<>
9
<style jsx>{`
10
.container {
11
max-width: 65rem;
12
margin: 1.5rem auto;
13
padding-left: 1rem;
14
padding-right: 1rem;
15
}
16
`}
17
18
</style>
19
<div className="container">
20
<header>
21
{/* Mount the UserButton component */}
22
<UserButton />
23
</header>
24
<main>Hello, {firstName}!</main>
25
</div>
26
</>
27
28
);
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.

The sign-in page with Clerk

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.

The UserButton component

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.

The account page

Adding SSO Using Clerk

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.

The Social Login page in Clerk

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.

Configuring Google SSO in Clerk

Save the settings, and you should now have a Sign in with Google button in your application.

Sign-in page with SSO in Clerk

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.

Conclusion

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.

Preview
Clerk's logo

Start Now,
No Strings Attached

Start completely free for up to 5,000 monthly active users and up to 10 monthly active orgs. No credit card required.

Start building

Pricing built for

businesses of all sizes.

Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.

Clerk's logo

Newsletter!

The latest news and updates from Clerk, sent to your inbox.