Frontend Authentication Integration
Complete guide for integrating authentication in the Next.js frontend with API clients, state management, and protected routes
Frontend Authentication Integration
Complete guide for integrating authentication in the Next.js frontend.
API Clients
Auth Client
For allauth headless endpoints:
// lib/api-client.ts
export const authClient = Axios.create({
baseURL: `${API_BASE_URL}/_allauth/browser/v1`,
withCredentials: true, // Send cookies
headers: {
"Content-Type": "application/json",
},
});
// Add CSRF token to requests
authClient.interceptors.request.use((config) => {
const csrfToken = getCsrfToken();
if (csrfToken) {
config.headers["X-CSRFToken"] = csrfToken;
}
return config;
});Main API Client
For authenticated API requests:
export const apiClient = Axios.create({
baseURL: `${API_BASE_URL}/api/v1`,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});Auth State Management
The useAuth hook provides all authentication functionality:
// hooks/use-auth.ts
export function useAuth() {
return {
// State
user: User | null,
isAuthenticated: boolean,
isLoading: boolean,
error: string | null,
config: AuthConfig | null,
pendingFlow: AuthFlow | null,
// Actions
login: (email: string, password: string) => Promise<void>,
signup: (email: string, password: string) => Promise<void>,
logout: () => Promise<void>,
getSession: () => Promise<void>,
getConfig: () => Promise<void>,
requestPasswordReset: (email: string) => Promise<void>,
resetPassword: (key: string, password: string) => Promise<void>,
verifyEmail: (key: string) => Promise<void>,
resendVerificationEmail: (email: string) => Promise<void>,
authenticateMfa: (code: string) => Promise<void>,
setupTotp: () => Promise<{ totp_url: string; secret: string }>,
activateTotp: (code: string) => Promise<void>,
socialLogin: (provider: string, callbackUrl: string) => void,
clearError: () => void,
};
}Auth Layout
Protected route wrapper with authentication check:
// app/(auth)/layout.tsx
export default function AuthLayout({ children }) {
const router = useRouter();
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
useAuthStore.persist.rehydrate();
useAuthStore.getState().getConfig(); // Establishes CSRF cookie
}, []);
useEffect(() => {
if (mounted && isAuthenticated) {
router.push("/projects");
}
}, [isAuthenticated, mounted, router]);
if (!mounted || isAuthenticated) {
return <LoadingSpinner />;
}
return <div className="auth-layout">{children}</div>;
}Login Page
// app/(auth)/login/page.tsx
export default function LoginPage() {
const router = useRouter();
const { login, socialLogin, isLoading, error, pendingFlow, config } = useAuth();
const [formData, setFormData] = useState({ email: "", password: "" });
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
try {
await login(formData.email, formData.password);
if (pendingFlow?.id === "mfa_authenticate") {
router.push("/mfa/verify");
} else {
router.push("/projects");
}
} catch {
// Error displayed from store
}
}
return (
<form onSubmit={onSubmit}>
<Input name="email" type="email" required />
<Input name="password" type="password" required />
{error && <p className="text-red-500">{error}</p>}
<Button type="submit" disabled={isLoading}>
{isLoading ? <Spinner /> : "Sign In"}
</Button>
</form>
);
}Protected Routes
App Layout
// app/(app)/layout.tsx
export default function AppLayout({ children }) {
const { isAuthenticated, isLoading, getSession } = useAuth();
const router = useRouter();
useEffect(() => {
getSession();
}, []);
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push("/login");
}
}, [isLoading, isAuthenticated, router]);
if (isLoading || !isAuthenticated) {
return <LoadingSpinner />;
}
return <>{children}</>;
}CSRF Token Handling
The frontend must:
- Fetch config first — This sets the CSRF cookie
- Include token in requests — Via
X-CSRFTokenheader
// Get CSRF token from cookie
function getCsrfToken(): string | null {
if (typeof document === "undefined") return null;
const match = document.cookie.match(/csrftoken=([^;]+)/);
return match ? match[1] : null;
}Session Persistence
Auth state is persisted to localStorage via Zustand:
export const useAuthStore = create<AuthStore>()(
persist(
(set, get) => ({
// ... state and actions
}),
{
name: "auth-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
skipHydration: true, // Manual hydration on mount
}
)
);Error Handling
const handleAuthError = (error: unknown): string => {
if (error instanceof Error) {
const axiosError = error as AxiosError<{
detail?: string;
errors?: { message: string }[];
}>;
if (axiosError.response?.data?.detail) {
return axiosError.response.data.detail;
}
if (axiosError.response?.data?.errors?.[0]?.message) {
return axiosError.response.data.errors[0].message;
}
return error.message;
}
return "An unexpected error occurred";
};Type Definitions
export type User = {
id: number;
email: string;
username?: string;
display?: string;
has_usable_password?: boolean;
};
export type AuthFlow = {
id: string;
is_pending?: boolean;
};
export type AuthConfig = {
account: {
authentication_method: string;
};
socialaccount: {
providers: { id: string; name: string; flows: string[] }[];
};
mfa: {
supported_types: string[];
};
};Testing
// hooks/use-auth.test.tsx
import { renderHook, act } from "@testing-library/react";
import { useAuth } from "./use-auth";
describe("useAuth", () => {
it("should login successfully", async () => {
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login("user@example.com", "password");
});
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user?.email).toBe("user@example.com");
});
});Last updated on