Auth.js v5 doesn’t just add user accounts to your Next.js app; it fundamentally rethinks how session management works, moving from server-side cookies to a more robust, client-side token-based approach that can feel like magic until you understand the pieces.

Let’s see it in action. Imagine a simple app/page.js:

import { auth } from "@/auth";
import { SignInButton } from "@/components/SignInButton";
import { SignOutButton } from "@/components/SignOutButton";

export default async function HomePage() {
  const session = await auth();

  return (
    <div>
      <h1>Welcome</h1>
      {session?.user ? (
        <div>
          <p>Hello, {session.user.name}!</p>
          <SignOutButton />
        </div>
      ) : (
        <SignInButton />
      )}
    </div>
  );
}

And our components/SignInButton.js:

import { signIn } from "@/auth";

export function SignInButton() {
  return (
    <form
      action={async () => {
        "use server";
        await signIn("github"); // Or any other provider
      }}
    >
      <button type="submit">Sign in with GitHub</button>
    </form>
  );
}

And components/SignOutButton.js:

import { signOut } from "@/auth";

export function SignOutButton() {
  return (
    <form
      action={async () => {
        "use server";
        await signOut();
      }}
    >
      <button type="submit">Sign out</button>
    </form>
  );
}

When you click "Sign in with GitHub," the signIn("github") action is triggered. Auth.js initiates the OAuth flow with GitHub. Upon successful authentication, GitHub redirects back to your application. Instead of Auth.js directly setting a server-side HTTP-only cookie, it now generates a JWT (JSON Web Token) containing your user’s session data and instructs the browser to store this token in localStorage. Subsequent requests from your client-side code that need authentication will read this token from localStorage and include it in the Authorization: Bearer <token> header. The auth() function on the server-side then decodes this token to reconstruct the session.

The core problem Auth.js v5 solves is creating a secure, flexible, and scalable authentication system for modern web applications, especially those using server components and dynamic client-side interactions. It abstracts away the complexities of OAuth, JWT management, and session persistence, allowing developers to focus on user experience. The auth() function, when called on the server (as in app/page.js), acts as your gateway to the current user’s session. It transparently handles token validation and retrieval from localStorage if needed, or it can directly access server-side session data if it’s available through other means (like a server-side session store).

The auth.js file in your app directory (or wherever you configure it, typically src/auth.js or app/auth.js) is the central configuration point.

// app/auth.js
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    // Add other providers here
  ],
  // Optional: configure callbacks, session management, etc.
  callbacks: {
    async jwt({ token, user, account, profile, isNewUser }) {
      // Add custom data to the token
      if (account) {
        token.provider = account.provider;
        token.providerAccountId = account.providerAccountId;
      }
      return token;
    },
    async session({ session, token }) {
      // Send properties to the client, like an access_token from a provider.
      session.user.id = token.sub; // 'sub' is the standard JWT subject claim, usually the user ID
      session.user.provider = token.provider;
      return session;
    },
  },
  session: {
    strategy: "jwt", // Default for v5, but good to be explicit
  },
});

This setup defines which authentication providers your application will support (GitHub in this case) and how user data is processed and passed between the server and client. The callbacks.jwt function is where you can enrich the JSON Web Token with additional information during the sign-in process. The callbacks.session function then uses this enriched token to shape the session object that’s ultimately available on the client and in server components. Notice how token.sub is mapped to session.user.id – this is a standard practice for linking the JWT’s subject claim to your application’s user representation.

A critical detail often overlooked is how Auth.js v5 handles session renewal. Because sessions are primarily managed via tokens in localStorage, the client-side JavaScript running in your browser is responsible for periodically checking the token’s validity and potentially refreshing it. This is usually handled by an internal Auth.js mechanism that listens for browser events and makes silent requests to your backend to validate the token. If the token expires, the user will be logged out automatically. However, if you’re performing client-side data fetching that relies on the session, and the token expires between the fetch and the server processing it, you might encounter unexpected errors. This is why robust error handling around authenticated API routes or server actions is important.

The next logical step after setting up basic authentication is implementing protected routes, ensuring that only authenticated users can access certain pages or resources.

Want structured learning?

Take the full Nextjs course →