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 aboveToken Configuration
Tokens are configured on the Django backend:
| Token | Lifetime | Setting |
|---|---|---|
| Access | 100 hours | ACCESS_TOKEN_LIFETIME |
| Refresh | 14 days | REFRESH_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 inClient-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>;
}