HyperSaaS
FrontendAuthentication

NextAuth Configuration

Credentials provider and JWT callback setup.

Provider Setup

NextAuth is configured in lib/auth/providers.ts with a single credentials provider:

import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        // 1. Request JWT tokens from Django
        const tokenRes = await fetch(`${BACKEND_URL}/auth/jwt/create/`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            email: credentials.email,
            password: credentials.password,
          }),
        });

        if (!tokenRes.ok) return null;
        const tokens = await tokenRes.json();

        // 2. Fetch user profile
        const userRes = await fetch(`${BACKEND_URL}/auth/users/me/`, {
          headers: { Authorization: `JWT ${tokens.access}` },
        });

        if (!userRes.ok) return null;
        const user = await userRes.json();

        // 3. Return user + tokens
        return {
          ...user,
          accessToken: tokens.access,
          refreshToken: tokens.refresh,
        };
      },
    }),
  ],
  // ...callbacks
});

JWT Callback

The JWT callback manages token storage and refresh:

callbacks: {
  async jwt({ token, user, trigger }) {
    // First sign-in: store tokens from authorize()
    if (user) {
      token.accessToken = user.accessToken;
      token.refreshToken = user.refreshToken;
      token.accessTokenExpires = Date.now() + ACCESS_TOKEN_LIFETIME;
      token.user = user;
      return token;
    }

    // Token still valid: return as-is
    if (Date.now() < token.accessTokenExpires - 60_000) {
      return token;
    }

    // Token expiring: refresh
    try {
      const refreshed = await refreshToken(token.refreshToken);
      token.accessToken = refreshed.access;
      token.accessTokenExpires = Date.now() + ACCESS_TOKEN_LIFETIME;
      return token;
    } catch {
      // Refresh failed: invalidate session
      return { ...token, error: "RefreshTokenError" };
    }
  },
}

Session Callback

The session callback exposes the token data to client components:

callbacks: {
  async session({ session, token }) {
    session.user = {
      ...session.user,
      ...token.user,
      accessToken: token.accessToken,
      refreshToken: token.refreshToken,
      accessTokenExpires: token.accessTokenExpires,
    };
    if (token.error) {
      session.error = token.error;
    }
    return session;
  },
}

Session Error Handler

The SessionErrorHandler component watches for refresh token failures:

function SessionErrorHandler({ children }) {
  const { data: session } = useSession();

  useEffect(() => {
    if (session?.error === "RefreshTokenError") {
      toast.error("Session expired. Please sign in again.");
      signOut({ callbackUrl: "/login" });
    }
  }, [session?.error]);

  return children;
}

Pages Configuration

pages: {
  signIn: "/login",        // Custom login page
  error: "/login",         // Redirect errors to login
  newUser: "/dashboard",   // Redirect after first sign-in
},

Session Strategy

session: {
  strategy: "jwt",         // JWT-only (no database sessions)
  maxAge: 14 * 24 * 60 * 60, // 14 days
},

On this page