In any modern web application authentication and authorization can be considered core implementations. Authentication simply refers to the user being logged in to websites and ensuring the valid or verified user is surfing the website and authorization means permission is given to that user based on his role and subscriptions in that website.
Below we will discuss step by step the way of authenticating an authorization of a user in a next js application which can be referred to for general application in any next.js.
Create the next application by running the below command
npx create-next-app@latest use-auth-hook-in-next
Install JSON web token package for generating jwt token
npm i jsonwebtoken
Make a .env file in the root and enter the below details
# .env
JWT_SECRET_KEY=ilovejavascript
If you have chosen tailwind CSS for styling then you will have extra tailwind.config.js file with tailwind utilities imported in the global CSS file which you can update as below.
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
Make a header component which can be imported in the app layout as the header of the app
// src/components/index.ts
export { Header } from "./header";
// src/components/header.tsx
"use client";
import { useAuth } from "@/hooks";
import { logout } from "@/utilities/helper";
import Link from "next/link";
import React from "react";
import { useRouter } from 'next/navigation'
export const Header = () => {
const { isLoggedIn } = useAuth();
const router = useRouter()
return (
<div>
{isLoggedIn ? (
<h3 className="cursor-pointer" onClick={()=>logout(router)}>
LOGOUT
</h3>
) : (
<Link href={`/login`}>LOGIN</Link>
)}
</div>
);
};
Now modify your layout file as below
You will note the global CSS file being imported which applied the CSS written throughout the app and this file acts as the layout file for all pages in the next app
// src/app/layout.tsx
import { Header } from "@/components";
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata: 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}>
<div className="min-h-[100vh] min-w-[100vh]">
<Header />
{children}
</div>
</body>
</html>
);
}
Now make a login API for generating a jwt token and return a token with user details.
This jwt token is generally used by servers to authenticate and authorize users as it contains user information to verify the user is valid and authorize to access particular resources on a website.
Below we have made a post API which takes the name, email and password as payload and returns jwt token with user details.
// src/app/api/login/route.ts
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export async function POST(req: Request) {
const { name, email, password } = await req.json();
let jwtSecretKey: any = process.env.JWT_SECRET_KEY;
let data = {
time: Date(),
userId: 1,
name,
email,
role: name === "admin" && password === "admin@123" ? "admin" : "user",
};
const token = jwt.sign(data, jwtSecretKey);
return NextResponse.json({ token: token, data });
}
Now make a login page as below
Here we make a basic login form to accept user details and then generate user details and jwt tokens to be stored in local storage and accessed throughout the app wherever required instead of generating them again and again.
// src/app/login/page.tsx
"use client";
import { useAuth } from "@/hooks";
import { appConstants } from "@/utilities/constants";
import { redirect } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
const Login = () => {
const router = useRouter();
const [formInput, setFormInput] = useState({
name: "",
email: "",
password: "",
});
const { isLoggedIn } = useAuth();
useEffect(() => {
if (isLoggedIn) return redirect("/");
}, [isLoggedIn]);
const handleSubmit = async (ev: any) => {
ev.preventDefault();
const headers = {
"Content-Type": "application/json",
};
const body = {
...formInput,
};
const data = await fetch("http://localhost:3000/api/login", {
headers,
method: "POST",
body: JSON.stringify({ body }),
}).then((result) => result.json());
localStorage.setItem(appConstants.USER_DETAILS_KEY, JSON.stringify(data));
return router.push("/");
};
const handleChange = (ev: any, fieldName: string) => {
setFormInput({ ...formInput, [fieldName]: ev.target.value });
};
return (
<div className="h-[100vh] w-[100vw] flex justify-center items-center">
<form
onSubmit={handleSubmit}
className="p-10 border flex flex-col items-center gap-5 shadow-2xl rounded bg-blue-50"
>
<div className="flex flex-col gap-3">
<label htmlFor="name">Name</label>
<input
required
type="text"
name="name"
id="name"
value={formInput.name}
onChange={(ev: any) => handleChange(ev, "name")}
/>
</div>
<div className="flex flex-col gap-3">
<label htmlFor="email">Email</label>
<input
required
type="email"
name="email"
id="email"
value={formInput.email}
onChange={(ev: any) => handleChange(ev, "email")}
/>
</div>
<div className="flex flex-col gap-3">
<label htmlFor="password">Password</label>
<input
required
type="password"
name="password"
id="password"
value={formInput.password}
onChange={(ev: any) => handleChange(ev, "password")}
/>
</div>
<button type="submit" className="rounded p-3 bg-orange-500 text-white">
Login
</button>
</form>
</div>
);
};
export default Login;
After login, you will be redirected to the home page which you can make below
// src/app/page.tsx
import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Home</h1>
<h3>
<u>
<Link href={`/content`}>CONTENT</Link>
</u>
</h3>
</div>
);
}
You can make useAuth hook to access local storage user details and access tokens and use it throughout the app wherever required to authenticate and authorize
// src/hooks/useAuth.ts
import { appConstants } from "@/utilities/constants";
import React, { useEffect, useState } from "react";
const initialState = {
isLoggedIn: false,
user: {},
};
export const useAuth = () => {
const data: any = localStorage.getItem(appConstants.USER_DETAILS_KEY);
const user = ((userDetails: any) => {
if (userDetails?.token && userDetails?.data) {
return { isLoggedIn: true, user: userDetails };
}
return initialState;
})(JSON.parse(data));
return {
isLoggedIn: user.isLoggedIn,
user: user.user,
};
};
// src/hooks/index.ts
export { useAuth } from "./useAuth";
Below is the constant file used for keeping app constants
// src/utilities/constants.ts
export const appConstants = {
USER_DETAILS_KEY: "userDetails",
};
Below we make a content page which uses the useAuth hook to check if a user is authenticated and redirects to the login page if not logged in.
// src/app/content/page.tsx
"use client";
import { useAuth } from "@/hooks";
import { redirect } from "next/navigation";
const Content = () => {
const { isLoggedIn, user } = useAuth();
if (!isLoggedIn) return redirect("login");
console.log(user);
return <div>Content</div>;
};
export default Content;
Below is the logout utility function which can be invoked when clicking on the logout button and redirecting user to the login page
// src/utilities/helper.ts
import { redirect } from "next/navigation";
export const logout = (router: any) => {
localStorage.clear();
return router.push("/login");
};
You can refer to below file structure for your app and package.json
//package.json
{
"name": "use-auth-hook-in-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "20.4.10",
"@types/react": "18.2.20",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"eslint": "8.47.0",
"eslint-config-next": "13.4.13",
"jsonwebtoken": "^9.0.1",
"next": "13.4.13",
"postcss": "8.4.27",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.1.6"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.2"
}
}
Hope you enjoyed this basic tut for making a hook to authenticate the user you can also use it to authorize a user based on his role stored in local storage as user details and make permission checks on different pages of your app