In this guide, I will show you how to secure your Next.js application with Userfront by implementing middleware-level authorization and leaving component authorization practices.
Implementing robust security measures like IAM is essential for protecting user data and maintaining compliance. This builds trust with your users and safeguards your application from potential vulnerabilities.
Many developers overlook comprehensive IAM solutions, opting for basic authentication methods that can expose their applications to significant security risks.
Key Takeaways
- Explore Userfront’s features like SSO, multi-tenancy, two-factor authentication, and custom JWTs—all available with a generous free tier.
- Understand the drawbacks of component-level user authorization.
- Ensure middleware-level JWT verification for robust authorization.
As applications grow in complexity, robust security becomes increasingly crucial. Userfront provides a modern IAM solution that integrates seamlessly with Next.js. This guide walks you through integrating Userfront authentication, emphasizing best practices for a secure setup.
Understanding Userfront in the IAM Landscape
Userfront simplifies the authentication process for developers with its easy-to-use SDK. Key features include:
- Single Sign-On (SSO): Users can log in once and access multiple applications without re-authenticating.
- Multi-Tenancy: Supports multiple clients with a single Userfront account, ideal for SaaS applications.
- Two-Factor Authentication (2FA): Adds an extra layer of security by requiring a second form of verification.
- SOC 2 Compliance: Adheres to strict data privacy and security standards.
Getting Started with Userfront
The Project is already settled up with Userfront. Clone the repository from this link. To learn about the fundamentals of setting up Userfront with Next.js, refer to the official documentation https://userfront.com/docs/examples/next.
Setting Up Userfront in Your Next.js Application
Environment Variables
Create an .env
file in your project root:
# WorkspaceId can be found at https://userfront.com/dashboard
NEXT_PUBLIC_USERFRONT_WORKSPACE_ID="..."
# Public JWT key can be found at https://userfront.com/test/dashboard/jwt?tab=public
# Make sure to get Base64 encoded public key
JWT_PUBLIC_KEY_BASE64="..."
Project Structure
app
├── (auth)
│ ├── layout.tsx
│ ├── login
│ │ └── page.tsx
│ ├── reset
│ │ └── page.tsx
│ └── signup
│ └── page.tsx
├── (public)
│ └── home
│ └── page.tsx
├── (secure)
│ └── dashboard
│ └── page.tsx
├── _components
│ └── Header.tsx
├── globals.css
└── layout.tsx
Directory Breakdown
- (auth): Contains authentication-related pages (login, signup, reset).
- (public): Public-facing pages (like home) accessible without authentication.
- (secure): Pages requiring authentication (like the dashboard).
Why Component-Level Authorization is Problematic
Consider this approach:
// app/(secure)/layout.tsx
"use client";
import * as React from "react";
import { useRouter } from "next/navigation";
import { useUserfront } from "@userfront/next/client";
export default function SecureLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const { isAuthenticated, isLoading } = useUserfront();
React.useEffect(() => {
if (isAuthenticated || isLoading || !router) return;
router.push("/login");
}, [isAuthenticated, isLoading, router]);
if (!isAuthenticated || isLoading) {
return null;
}
return children;
}
Drawbacks
- Redirects: Unnecessary redirects and allowing component access for a split second leaving security flaws and bad UX.
- Insecure Logic: Client-side checks can be manipulated, exposing sensitive data.
- Race Conditions: Delays in loading may expose restricted content.
- Performance Issues: Unnecessary rendering can slow down your application.
- Complexity: Mixing authentication logic with UI rendering complicates maintenance.
- Scalability: Harder to manage as the application grows.
Implementing Middleware for Authentication
Create a middleware.ts
file for JWT verification:
// middleware.ts
"use server";
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify, importSPKI, JWTPayload } from "jose";
const JWT_PUBLIC_KEY_BASE64 = process.env.JWT_PUBLIC_KEY_BASE64!;
const WORKSPACE_ID = process.env.NEXT_PUBLIC_USERFRONT_WORKSPACE_ID
interface UserFrontJwtPayload extends JWTPayload {
userId?: string;
}
async function verifyToken(token: string, publicKeyBase64: string) {
try {
const publicKey = await importSPKI(
Buffer.from(publicKeyBase64, "base64").toString("utf-8"),
"RS256"
);
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
});
return payload as UserFrontJwtPayload;
} catch (error) {
console.log("JWT verification failed:", error);
return null;
}
}
const pathsToExclude = /^(?!\/(api|_next\/static|favicon\.ico|manifest|icon|static|mergn)).*$/;
const publicPagesSet = new Set<string>(["/home"]);
const privatePagesSet = new Set<string>(["/dashboard"]);
const rootRegex = /^\/($|\?.+|#.+)?$/;
export default async function middleware(req: NextRequest) {
if (!pathsToExclude.test(req.nextUrl.pathname) || publicPagesSet.has(req.nextUrl.pathname)) {
return NextResponse.next();
}
const accessToken = req.cookies.get(`access.${WORKSPACE_ID}`)?.value;
const decoded = accessToken ? await verifyToken(accessToken, JWT_PUBLIC_KEY_BASE64) : null;
const isAuthenticated = decoded && decoded.userId;
if (rootRegex.test(req.nextUrl.pathname)) {
return isAuthenticated ? NextResponse.redirect(new URL("/dashboard", req.url)) : NextResponse.redirect(new URL("/login", req.url));
}
if (privatePagesSet.has(req.nextUrl.pathname) && !isAuthenticated) {
return NextResponse.redirect(new URL("/login", req.url));
}
if (req.nextUrl.pathname.startsWith("/login") && isAuthenticated) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
}
Middleware Explained
Middleware processes requests before they reach routes, centralizing authentication logic. Key functions include:
- JWT Verification: Ensures only authenticated users can access protected routes.
- Path Exclusions: Optimizes performance by excluding public routes from authentication checks.
- Redirect Logic: Manages user redirection based on authentication status.
Conclusion
By following these steps, you can effectively secure your Next.js application with Userfront's IAM capabilities. Middleware enhances security, ensuring only authorized users access sensitive routes while providing a seamless user experience.
For more details on the Userfront and its features, visit the Userfront documentation. With Userfront’s generous free tier, you can explore advanced features like SSO, multi-tenancy, and more without any initial investment.
That’s it! I hope you found this guide helpful. 🚀
Feel free to follow me on GitHub and LinkedIn for more guides.