Skip to main content

Command Palette

Search for a command to run...

Securing Next.js Apps: A Journey with JWT and Server-Side Cookies

Published
5 min read
Securing Next.js Apps: A Journey with JWT and Server-Side Cookies
M

I am a highly qualified software developer with over 6 years of experience in developing desktop and web applications using various technologies.My main programming language is JavaScript, and I am currently working as a Front-end developer at Heybooster. I am passionate about software development and constantly seek to improve my skills and knowledge through research and self-teaching. I share my projects on Github as open source to not only learn and grow, but also contribute to the development community. As a software developer, I am committed to upholding ethical standards in my work.

In this blog post, I will dive deep into my authentication solution implemented in our heybooster project. We'll explore how it utilizes JSON Web Tokens (JWT) stored securely in server-side cookies, and how state management is handled using Zustand.

1. Login API

Let's start at the beginning - the login process. Once a user logs in successfully, the server sends back a JWT. I've ensured that this token is securely stored in a server-side cookie.

// /api/auth/login/route.ts
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import api from "@/_services/loginApiService";
function setJWTCookie(jwt: string, date: Date) {
    cookies().set({
        name: 'jwt',
        value: jwt,
        httpOnly: true,
        secure: true,
        path: '/',
        expires: date
    });
}

export async function POST(request: Request) {
// ... Rest of the login logic ...
    setJWTCookie(jwt,date);
// ... Rest of the login logic ...
}

The setJWTCookie function I developed ensures that the JWT is securely stored with properties like httpOnly and secure.

2. Checking Authentication Status

To determine if a user is authenticated, I inspect the JWT in the server-side cookie.

// /api/auth/check/route.ts
export async function GET(request: NextRequest) {
    const jwt: string | undefined = cookies().get(CookieKeys.Jwt)?.value;
    const isAuthenticated: boolean = !!jwt;
    // ... Rest of the check logic ...
}

3. Logging Out

The logout process I've implemented is clear-cut: remove the JWT from the server-side cookie.

// /api/auth/logout/route.ts
export async function POST(request: NextRequest) {
    cookies().delete(CookieKeys.Jwt);
    // ... Rest of the logout logic ...
}

4. Client-Side Logic

I've also established a custom hook named useAuthStatusFetcher. This is pivotal in fetching the authentication status from the server and then updating the Zustand store.

import axios from "axios";
import { useCallback } from "react";
import { useQuery } from "react-query";
import { useAuthStore } from "@/_store/useAuthStore";

export interface IResponse {
    isAuthenticated: boolean
}

const fetchStatus = async () => {
    let status: boolean = false;
    try {
        const { data } = await axios.get<IResponse>('/api/auth/check');
        status = data.isAuthenticated;
    } catch (error) {
        // ....
    }

    return status;
}

export default function useAuthStatusFetcher() {
    const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
    const { refetch, } = useQuery([ "isAuthenticated" ], fetchStatus, {
        enabled: false,
        onSuccess: (isAuthenticated) => {
            setIsAuthenticated(isAuthenticated)
        }
    });

    const fetchAuthStatus = useCallback(() => {
        refetch();
    }, [ refetch ]);

    return { fetchAuthStatus };
}

5. Zustand Store

For state management in the project, I turned to Zustand, a minimal yet powerful solution.

import { create, StoreApi } from 'zustand';

export interface State {
    isAuthenticated: boolean,
}

export interface Actions {
    setIsAuthenticated: (isAuthenticated: boolean) => void;
    reset: () => void
}

const initialState: State = {
    isAuthenticated: false
}

const createState = (set: StoreApi<State & Actions>["setState"]): State & Actions => ({
    ...initialState,
    setIsAuthenticated: (isAuthenticated) => set(({ isAuthenticated })),
    reset: () => set(initialState)
});

export const useAuthStore = create(createState);

6. Form Handling

After successful login, I've made sure that the form fetches the authentication status, updating the Zustand store accordingly.

"use client";
import { useForm } from 'react-hook-form';
import axios from "axios";
import useAuthStatusFetcher from "@/_hooks/auth/useAuthStatusFetcher";

interface IFormInput {
    username: string;
    password: string;
}

export default function PopupLogin() {
    const { fetchAuthStatus } = useAuthStatusFetcher();

    const {
        register,
        handleSubmit,
    } = useForm<IFormInput>();

    const onSubmit = async (data: IFormInput) => {
        const { username, password } = data;

        try {
            await axios.post('api/auth/login', { username, password });
            fetchAuthStatus();
            // rest of the code

        } catch (error) {
            //rest of code
        }
    };

    return (
        <div
            className="grid place-items-center overflow-x-hidden overflow-y-auto fixed inset-0 z-50 bg-neutral-800/70">
            <div className="relative w-full max-w-md px-4">
                <div className="bg-white rounded-lg shadow relative">
                    <form onSubmit={handleSubmit(onSubmit)}
                          className="space-y-6 px-6 lg:px-8 pb-4 sm:pb-6 xl:pb-8">
                        <h3 className="text-xl font-medium text-gray-900">Sign in</h3>
                        <div>
                            <label htmlFor="username"
                                   className="text-sm font-medium text-gray-900 block mb-2 ">Your
                                username</label>
                            <input type="text"
                                   id="username"
                                   {...register('username', { required: true })} />
                        </div>
                        <div>
                            <label htmlFor="password"
                                   className="text-sm font-medium text-gray-900 block mb-2 ">Your
                                password</label>
                            <input type="password"
                                   id="password"
                                   {...register('password', { required: true })}/>
                        </div>

                        <button type="submit">
                            Login
                        </button>
                    </form>
                </div>
            </div>
        </div>
    );
};

7. App State Initialization

An essential part of the experience is that the app begins with the correct authentication status. My AppStateInitializer component ensures this by fetching the status during the initial page load.

"use client";
import { ReactNode } from "react";
import AppStateInitializer from "@/_components/AppStateInitializer";

export default function Providers({ children }: { children: ReactNode }) {
    return (
            <AppStateInitializer/>
            {children}
    );
}

8. Handling Authentication in Components

To enhance the user experience, I've ensured components like the navbar dynamically change based on the authentication status.

"use client";
import { useAuthStore } from "@/_store/useAuthStore";

const Header = () => {
    const isAuthenticated = useAuthStore(state => state.isAuthenticated);

    return (
        <nav
            className="sticky z-10 top-0 h-16 w-full px-6 py-3 flex items-center justify-between border-b border-gray-100 bg-white">
            <div className="flex items-center gap-3">
                {isAuthenticated && (
                    <div>
                        ...rest of code
                    </div>
                )}
            </div>

            <div>
                {isAuthenticated ? (
                    <div>
                        ...rest of code
                    </div>
                ) : (
                    <div>
                        ...rest of code
                    </div>
                )}
            </div>
        </nav>
    );
};

export default Header;

9. Protecting Routes with High-Order Components (HOC)

Security is paramount. To ensure unauthenticated users cannot access specific pages, I crafted a High-Order Component (HOC) named withAuth.

import { ComponentType } from 'react';
import { useAuthStore } from "@/_store/useAuthStore";
import { useCommonStore } from "@/_store/useCommonStore";
import { redirect } from "next/navigation";

export default function withAuth(WrappedComponent: ComponentType<any>) {
    const AuthCheckComponent = (props: any) => {
        const isAuthenticated = useAuthStore(state => state.isAuthenticated);
        const apiStatuses = useCommonStore(state => state.apiStatuses);

        if (!apiStatuses?.isAuthenticated?.isLoaded) {
            return <Skeleton/>
        }

        if (!isAuthenticated) {
            redirect("/");
            return null;
        }

        return <WrappedComponent {...props} />;
    };

    AuthCheckComponent.displayName = `WithAuthCheck(${getDisplayName(WrappedComponent)})`;

    return AuthCheckComponent;
};

function getDisplayName(WrappedComponent: ComponentType<any>): string {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

Conclusion

Crafting an authentication system in Next.js that seamlessly integrates JWTs, server-side cookies, and Zustand was both a challenge and a triumph. As a single front-end developer, I delved deep into the intricacies of security and user experience, ensuring that each piece of the puzzle fit just right. This journey has been a testament to what's achievable with dedication, innovation, and a bit of code magic. If you're embarking on a Next.js project, I hope my solution inspires and guides your authentication endeavors. Here's to building secure and user-friendly web applications! 🌟

More from this blog

Mustafa Dalga - JavaScript Expert, Nature Photographer, Book Enthusiast, and Tech Blogger

92 posts

Sharing tech and life experiences as a software developer, reader, and nature photographer. Explore Mustafa's world of work and hobbies.