HyperSaaS
FrontendAuthentication

JWT Flow

Token lifecycle from login through refresh and expiry.

Token Lifecycle

Login


POST /auth/jwt/create/  →  {access, refresh}


Store in NextAuth JWT session


Use access token for API requests

  ├── Token valid → Request succeeds

  ├── Token expiring (< 60s left) → Auto-refresh
  │   │
  │   ▼
  │   POST /auth/jwt/refresh/  →  {access}
  │   │
  │   ├── Success → Update token, continue
  │   └── Failure → Invalidate session → Redirect to /login

  └── Token expired → Refresh attempt → Same as above

Token Configuration

Tokens are configured on the Django backend:

TokenLifetimeSetting
Access100 hoursACCESS_TOKEN_LIFETIME
Refresh14 daysREFRESH_TOKEN_LIFETIME

The frontend refreshes the access token 60 seconds before expiry to prevent failed requests.

Server-Side Token Access

Server components and API routes use getAccessToken() to extract the JWT:

// lib/auth/utils.ts
export async function getAccessToken(): Promise<string | null> {
  const session = await auth();
  return session?.user?.accessToken ?? null;
}

Usage in server components:

export default async function Page() {
  const token = await getAccessToken();
  const res = await fetch(`${BACKEND_URL}/api/workspaces/`, {
    headers: { Authorization: `JWT ${token}` },
    cache: "no-store",
  });
  // ...
}

Token Validation

The frontend can verify a token against the backend:

// lib/auth/utils.ts
export async function isJWTTokenValid(token: string): Promise<boolean> {
  const res = await fetch(`${BACKEND_URL}/auth/jwt/verify/`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ token }),
  });
  return res.ok;
}

Token Refresh

export async function refreshToken(refresh: string): Promise<{ access: string }> {
  const res = await fetch(`${BACKEND_URL}/auth/jwt/refresh/`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refresh }),
  });
  if (!res.ok) throw new Error("Token refresh failed");
  return res.json();
}

Login Flow

The login server action validates the form and calls NextAuth's signIn:

// lib/auth/actions.ts
export async function loginAction(formData: LoginFormData) {
  const validated = userLoginSchema.safeParse(formData);
  if (!validated.success) return { error: "Invalid input" };

  const result = await signIn("credentials", {
    email: validated.data.email,
    password: validated.data.password,
    redirect: false,
  });

  if (result?.error) return { error: "Invalid credentials" };
  return { success: true };
}

Registration Flow

Registration goes directly to the Django backend (no NextAuth involvement):

1. POST /auth/users/  →  Create user account
2. Backend sends activation email
3. User clicks activation link → /user-activation/[uid]/[token]
4. POST /auth/users/activation/  →  Activate account
5. User can now log in

Client-Side Session Access

Client components access the session via the useSession hook:

"use client";
import { useSession } from "next-auth/react";

function UserMenu() {
  const { data: session, status } = useSession();

  if (status === "loading") return <Skeleton />;
  if (!session) return <LoginButton />;

  return <span>{session.user.email}</span>;
}

On this page