This article delves into the step-by-step process of integrating NextAuth into a Next.js application, configuring authentication providers, and implementing the account switcher functionality.
Prerequisites
- Basic understanding of JavaScript and React(Next.js)
- Familiarity with NextAuth
- NodeJS v16 or later
Getting started
To get started, we would first need to initialise a Next.js project and install the necessary dependencies
Open your terminal and run the following command.
npx create-next-app@latest --ts
Although this example uses the Next.js app directory, the same approach also works with the Next.js pages directory.
Cd into your project directory and install Next-auth
npm install next-auth
Configuring authentication providers
Create a file at this path /app/api/auth/[...nextauth]/route.ts
. If you are using the pages directory you can follow this guide to configure your provider
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
// users DB
const usersDB = [
{
name: "John Done",
id: "1",
email: "[email protected]",
image: "",
},
{
name: "Michael Jackson",
id: "2",
email: "[email protected]",
image: "",
},
{
name: "Jack Hammer",
id: "3",
email: "[email protected]",
image: "",
},
];
const handler = NextAuth({
// Here is where we place our providers
providers: [
Credentials({
id: "account-switch",
name: "Account Switch",
credentials: {
id: {
label: "id",
placeholder: "Enter id",
type: "text",
value: "",
},
email: {
label: "email",
placeholder: "Enter email",
type: "email",
},
},
authorize(credentials, req) {
// Validate user
/**
* You can make an aditional request to your backend
* to verify this user i.e get a new access/refresh token etc.
*
*/
const user = usersDB.find((user) => {
return user.email === credentials?.email;
});
if (!user) throw new Error("Account not found!");
// Verify Token
/**
* Ideally this would be an api call to your backend to verify the token
*/
const isTokenValid = true;
if (!isTokenValid) throw new Error("Token Invalid! Log in again");
return {
name: user.name,
id: user.id,
email: user.email,
image: user.image,
};
},
}),
});
export { handler as GET, handler as POST }
We use the credentials provider to implement a custom authentication strategy. For example, if we want users to log in using their username, phone number, and password, we can pass those values and implement our own authentication logic.
Set up accounts manager
The accounts manager is responsible for storing and managing all the accounts that users have used on your app. You can use any state management solution such as Redux, Zustand, Jotai, React Context etc.
In this example, we used React Context as the tool for managing our account state.
Create an account-manager.tsx
file
import { User } from "next-auth";
import { ReactNode, createContext, useContext, useState } from "react";
const accountManager = createContext<{
accounts: User[];
addAccount: (account: User) => void;
}>({ accounts: [], addAccount: (v) => {} });
export default function AccountManagerProvider({
children,
}: {
children: ReactNode;
}) {
const [accounts, setAccounts] = useState<User[]>([
// Default accounts - just for testing
{
name: "John Done",
id: "1",
email: "[email protected]",
image: "",
},
{
name: "Michael Jackson",
id: "2",
email: "[email protected]",
image: "",
},
]);
return (
<accountManager.Provider
value={{
accounts,
// this can be used when a user logs in
addAccount: (account) => {
setAccounts((prev) => [...prev, account]);
},
}}
>
{children}
</accountManager.Provider>
);
}
// useAccountManager Hook
export function useAccountManager() {
return useContext(accountManager);
}
Set up your root providers
Providers serve as the source of data or state that can be accessed by child components. In that vain we can access our session and our accounts
Create a projects.tsx file and input the following
"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
import AccountManagerProvider from "./accounts-manager";
export default function Providers({ children }: { children: ReactNode }) {
return (
<SessionProvider>
<AccountManagerProvider>{children}</AccountManagerProvider>
</SessionProvider>
);
}
Wrap the children in your root layout.tsx file with the providers - this allows us access to the session data and user accounts state.
import "./globals.css";
import { Inter } from "next/font/google";
import Providers from "./providers";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
Setup Switch Account Component
This component allows you to use to select an account and switch between them using the signIn
method from next-auth/react .
Note: the select component is from ui.shadcn.com but any select component should be us effective.
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { useAccountManager } from "./accounts-manager";
import { signIn, useSession } from "next-auth/react";
import Link from "next/link";
import type { Session } from "next-auth";
export function AccountSwitcher({ session }: { session?: Session | null }) {
****// You can pass the session in from a server component to avoid a rerender but you can use the useSession hook also
// const { data:session } = useSession();
const { accounts } = useAccountManager();
const currentAccount = accounts.find(
(account) => account.name === session?.user?.name
);
return (
<Select
defaultValue={currentAccount?.id}
onValueChange={async (value) => {
const account = accounts.find((account) => account.id === value);
if (!account) throw new Error("Account not found!");
try {
const result = await signIn("account-switch", { ...account });
} catch (error) {
console.log(error);
}
}}
>
<SelectTrigger className="w-[180px]" autoFocus>
<SelectValue placeholder="Select an account" />
</SelectTrigger>
<SelectContent>
{accounts.length < 1 ? (
<p className="text-center">
<Link href="/login" className="text-sm underline">
Login
</Link>
</p>
) : (
<>
{accounts.map((account) => {
return (
<SelectItem key={account.id} value={account.id}>
{account.name}
</SelectItem>
);
})}
</>
)}
</SelectContent>
</Select>
);
}
Update your page
Import the AccountSwitcher in any component.
import { getServerSession } from "next-auth";
import { AccountSwitcher } from "./account-switcher";
export default async function Home() {
const session = await getServerSession();
return (
<main className="container">
<div className="flex min-h-screen flex-col items-center justify-center">
<div className="mb-4 text-center">
{session ? `Currently Signed in as ${session.user?.name}` : null}
</div>
<AccountSwitcher session={session} />
</div>
</main>
);
}
Possible Improvements
- To persist user accounts, you can save the data in a storage solution like LocalStorage/Redis.
- Security - It might not be best to store your authorisation token in LocalStorage. Instead, you can store each user’s token in a cookie with a unique identifier, such as _userId:token. To manage cookies in Next.js, you can use the cookie helpers provided by Next.js
Code Repository
Here is a link to the code repository.