Protected Routes Example

Complete example of implementing protected routes and requireAuth middleware.

Protecting Routes with Authentication

This example demonstrates how to protect routes and API endpoints using the requireAuth middleware. Protected routes ensure only authenticated users can access specific pages and API endpoints.

How Route Protection Works

  1. User requests a protected route
  2. Middleware checks for valid session cookie
  3. If authenticated, request proceeds to route handler
  4. If not authenticated, user is redirected to login

Next.js Client-Side Protection

Protect client-side routes using the useAuth hook:

Protected Dashboard Page

Client-side route protection with useAuth

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function DashboardPage() {
  const { session, loading, signOut } = useAuth();
  const router = useRouter();

  // Redirect to login if not authenticated
  useEffect(() => {
    if (!loading && !session) {
      router.push("/login");
    }
  }, [session, loading, router]);

  // Show loading state
  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
      </div>
    );
  }

  // Don't render until we know we're authenticated
  if (!session) {
    return null; // Will redirect to login
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <nav className="bg-white shadow">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between h-16">
            <div className="flex items-center">
              <h1 className="text-xl font-bold">Dashboard</h1>
            </div>
            <div className="flex items-center space-x-4">
              <span className="text-sm text-gray-700">
                {session.user.email}
              </span>
              <button
                onClick={signOut}
                className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700"
              >
                Sign Out
              </button>
            </div>
          </div>
        </div>
      </nav>

      <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        <div className="px-4 py-6 sm:px-0">
          <div className="bg-white rounded-lg shadow p-6">
            <h2 className="text-2xl font-bold mb-4">
              Welcome, {session.user.name}!
            </h2>
            <p className="text-gray-600">
              This is a protected page. Only authenticated users can see this content.
            </p>
          </div>
        </div>
      </main>
    </div>
  );
}

Reusable Protection Component

Create a reusable component for protecting multiple pages:

components/auth/ProtectedRoute.tsx

Reusable protected route wrapper component

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { useEffect, ReactNode } from "react";

interface ProtectedRouteProps {
  children: ReactNode;
  redirectTo?: string;
  requiredRole?: string;
}

export function ProtectedRoute({
  children,
  redirectTo = "/login",
  requiredRole,
}: ProtectedRouteProps) {
  const { session, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !session) {
      router.push(redirectTo);
    }

    // Optional: Check for required role
    if (!loading && session && requiredRole) {
      const userRole = (session as any).role;
      if (userRole !== requiredRole) {
        router.push("/unauthorized");
      }
    }
  }, [session, loading, router, redirectTo, requiredRole]);

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
      </div>
    );
  }

  if (!session) {
    return null;
  }

  return <>{children}</>;
}

// Usage in a page:
// export default function DashboardPage() {
//   return (
//     <ProtectedRoute>
//       <div>Protected content here</div>
//     </ProtectedRoute>
//   );
// }

Next.js Server-Side Protection

Protect server components and API routes:

app/dashboard/page.tsx (Server Component)

Server-side route protection

import { getServerSession } from "@warpy-auth-sdk/core/hooks/server";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const session = await getServerSession();

  if (!session) {
    redirect("/login");
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <nav className="bg-white shadow">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between h-16">
            <div className="flex items-center">
              <h1 className="text-xl font-bold">Dashboard</h1>
            </div>
            <div className="flex items-center">
              <span className="text-sm text-gray-700">
                {session.user.email}
              </span>
            </div>
          </div>
        </div>
      </nav>

      <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-2xl font-bold mb-4">
            Welcome, {session.user.name}!
          </h2>
          <pre className="bg-gray-100 p-4 rounded overflow-auto">
            {JSON.stringify(session, null, 2)}
          </pre>
        </div>
      </main>
    </div>
  );
}

Express Route Protection

Protect Express routes using the requireAuth middleware:

Express Protected Routes

Implementing requireAuth in Express

import express from "express";
import cookieParser from "cookie-parser";
import { registerAuthRoutes } from "@warpy-auth-sdk/core/adapters/express";
import { google, type AuthConfig } from "@warpy-auth-sdk/core";

const app = express();

app.use(express.json());
app.use(cookieParser());

// Configure authentication
const authConfig: AuthConfig = {
  secret: process.env.AUTH_SECRET!,
  provider: google({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    redirectUri: process.env.GOOGLE_REDIRECT_URI!,
  }),
};

// Register auth routes and get requireAuth middleware
const { requireAuth } = registerAuthRoutes(app, authConfig, {
  basePath: "/api/auth",
  successRedirect: "/dashboard",
  errorRedirect: "/login",
});

// Public route
app.get("/api/public", (req, res) => {
  res.json({ message: "This is public" });
});

// Protected single route
app.get("/api/user", requireAuth, (req, res) => {
  res.json({ user: (req as any).session.user });
});

// Protected route with additional middleware
app.get("/api/admin",
  requireAuth,
  (req, res, next) => {
    const session = (req as any).session;
    if (session.user.role !== 'admin') {
      return res.status(403).json({ error: "Forbidden" });
    }
    next();
  },
  (req, res) => {
    res.json({ message: "Admin only content" });
  }
);

// Protect entire route group
const protectedRouter = express.Router();
protectedRouter.use(requireAuth);

protectedRouter.get("/profile", (req, res) => {
  res.json({ user: (req as any).session.user });
});

protectedRouter.get("/settings", (req, res) => {
  res.json({ settings: "User settings" });
});

protectedRouter.post("/update", (req, res) => {
  res.json({ success: true });
});

app.use("/api/protected", protectedRouter);

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Fastify Route Protection

Protect Fastify routes using the preHandler hook:

Fastify Protected Routes

Implementing requireAuth in Fastify

import fastify from "fastify";
import fastifyCookie from "@fastify/cookie";
import { registerAuthPlugin } from "@warpy-auth-sdk/core/adapters/fastify";
import { google, type AuthConfig } from "@warpy-auth-sdk/core";

const app = fastify({ logger: true });

await app.register(fastifyCookie);

// Configure authentication
const authConfig: AuthConfig = {
  secret: process.env.AUTH_SECRET!,
  provider: google({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    redirectUri: process.env.GOOGLE_REDIRECT_URI!,
  }),
};

// Register auth plugin and get requireAuth
const { requireAuth } = registerAuthPlugin(app, authConfig, {
  basePath: "/auth",
  successRedirect: "/dashboard",
  errorRedirect: "/login",
});

// Public route
app.get("/api/public", async (request, reply) => {
  return { message: "This is public" };
});

// Protected single route
app.get("/api/user", { preHandler: requireAuth }, async (request) => {
  return { user: (request as any).session.user };
});

// Protected route with custom role check
app.get("/api/admin",
  {
    preHandler: [
      requireAuth,
      async (request, reply) => {
        const session = (request as any).session;
        if (session.user.role !== 'admin') {
          reply.code(403).send({ error: "Forbidden" });
        }
      },
    ],
  },
  async () => {
    return { message: "Admin only content" };
  }
);

// Protect route group with plugin
await app.register(async (protectedRoutes) => {
  protectedRoutes.addHook("preHandler", requireAuth);

  protectedRoutes.get("/profile", async (request) => {
    return { user: (request as any).session.user };
  });

  protectedRoutes.get("/settings", async () => {
    return { settings: "User settings" };
  });

  protectedRoutes.post("/update", async () => {
    return { success: true };
  });
}, { prefix: "/api/protected" });

await app.listen({ port: 3000, host: "0.0.0.0" });

Hono Route Protection

Protect Hono routes using middleware:

Hono Protected Routes

Implementing requireAuth in Hono

import { Hono } from "hono";
import { createAuthHandler } from "@warpy-auth-sdk/core/adapters/hono";
import { google, type AuthConfig } from "@warpy-auth-sdk/core";

const app = new Hono();

// Configure authentication
const authConfig: AuthConfig = {
  secret: process.env.AUTH_SECRET!,
  provider: google({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    redirectUri: process.env.GOOGLE_REDIRECT_URI!,
  }),
};

// Create auth handler and get requireAuth
const { handler, requireAuth } = createAuthHandler(authConfig, {
  basePath: "/api/auth",
  successRedirect: "/dashboard",
  errorRedirect: "/login",
});

// Register auth routes
app.route("/api/auth", handler);

// Public route
app.get("/api/public", (c) => {
  return c.json({ message: "This is public" });
});

// Protected single route
app.get("/api/user", requireAuth, (c) => {
  const session = c.get("session");
  return c.json({ user: session.user });
});

// Protected route with role check
app.get("/api/admin", requireAuth, (c) => {
  const session = c.get("session");
  if (session.user.role !== 'admin') {
    return c.json({ error: "Forbidden" }, 403);
  }
  return c.json({ message: "Admin only content" });
});

// Protect route group
const protectedRoutes = new Hono();
protectedRoutes.use("*", requireAuth);

protectedRoutes.get("/profile", (c) => {
  const session = c.get("session");
  return c.json({ user: session.user });
});

protectedRoutes.get("/settings", (c) => {
  return c.json({ settings: "User settings" });
});

protectedRoutes.post("/update", (c) => {
  return c.json({ success: true });
});

app.route("/api/protected", protectedRoutes);

export default app;

Node.js HTTP Route Protection

Protect pure Node.js routes:

Node.js Protected Routes

Implementing requireAuth in pure Node.js

import { createServer } from "http";
import { createAuthHandler } from "@warpy-auth-sdk/core/adapters/node";
import { google, type AuthConfig } from "@warpy-auth-sdk/core";

// Configure authentication
const authConfig: AuthConfig = {
  secret: process.env.AUTH_SECRET!,
  provider: google({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    redirectUri: process.env.GOOGLE_REDIRECT_URI!,
  }),
};

// Create auth handler and get requireAuth
const { handler: authHandler, requireAuth } = createAuthHandler(authConfig, {
  basePath: "/api/auth",
  successRedirect: "/dashboard",
  errorRedirect: "/login",
});

// Helper to send JSON
function sendJSON(res: any, data: unknown, status = 200) {
  res.statusCode = status;
  res.setHeader("content-type", "application/json");
  res.end(JSON.stringify(data));
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
  const pathname = url.pathname;

  // Try auth handler first
  const handled = await authHandler(req, res);
  if (handled) return;

  // Public route
  if (pathname === "/api/public" && req.method === "GET") {
    return sendJSON(res, { message: "This is public" });
  }

  // Protected route
  if (pathname === "/api/user" && req.method === "GET") {
    const authenticated = await requireAuth(req, res);
    if (!authenticated) return; // requireAuth already sent response
    return sendJSON(res, { user: req.session?.user });
  }

  // Protected admin route
  if (pathname === "/api/admin" && req.method === "GET") {
    const authenticated = await requireAuth(req, res);
    if (!authenticated) return;

    const session = req.session;
    if (session?.user.role !== 'admin') {
      return sendJSON(res, { error: "Forbidden" }, 403);
    }

    return sendJSON(res, { message: "Admin only content" });
  }

  // Protected route group
  if (pathname.startsWith("/api/protected/")) {
    const authenticated = await requireAuth(req, res);
    if (!authenticated) return;

    if (pathname === "/api/protected/profile" && req.method === "GET") {
      return sendJSON(res, { user: req.session?.user });
    }

    if (pathname === "/api/protected/settings" && req.method === "GET") {
      return sendJSON(res, { settings: "User settings" });
    }
  }

  // 404
  res.statusCode = 404;
  res.setHeader("content-type", "text/plain");
  res.end("Not Found");
});

server.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

Role-Based Access Control

Implement role-based authorization:

Role-Based Middleware

Create middleware for role-based access control

import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@warpy-auth-sdk/core";

// Define user roles
type UserRole = 'user' | 'admin' | 'moderator';

// Role hierarchy
const roleHierarchy: Record<UserRole, number> = {
  user: 1,
  moderator: 2,
  admin: 3,
};

export function requireRole(requiredRole: UserRole) {
  return async (req: NextRequest) => {
    const session = await getSession(req, process.env.AUTH_SECRET!);

    if (!session) {
      return NextResponse.json(
        { error: "Unauthorized" },
        { status: 401 }
      );
    }

    const userRole = (session as any).role as UserRole;
    const userLevel = roleHierarchy[userRole] || 0;
    const requiredLevel = roleHierarchy[requiredRole];

    if (userLevel < requiredLevel) {
      return NextResponse.json(
        { error: "Forbidden: Insufficient permissions" },
        { status: 403 }
      );
    }

    return null; // Allow access
  };
}

// Usage in API routes:
export async function GET(req: NextRequest) {
  const roleCheck = await requireRole('admin')(req);
  if (roleCheck) return roleCheck;

  // Admin-only logic here
  return NextResponse.json({ message: "Admin content" });
}

// For Express:
export function requireRoleExpress(requiredRole: UserRole) {
  return (req: any, res: any, next: any) => {
    const session = req.session;

    if (!session) {
      return res.status(401).json({ error: "Unauthorized" });
    }

    const userRole = session.role as UserRole;
    const userLevel = roleHierarchy[userRole] || 0;
    const requiredLevel = roleHierarchy[requiredRole];

    if (userLevel < requiredLevel) {
      return res.status(403).json({ error: "Forbidden" });
    }

    next();
  };
}

// Usage:
// app.get("/api/admin", requireAuth, requireRoleExpress('admin'), handler);

Permission-Based Access Control

Implement fine-grained permission checking:

Permission-Based Middleware

Check specific permissions instead of roles

type Permission =
  | 'read:users'
  | 'write:users'
  | 'delete:users'
  | 'read:posts'
  | 'write:posts'
  | 'delete:posts';

interface User {
  id: string;
  email: string;
  permissions: Permission[];
}

export function requirePermission(...requiredPermissions: Permission[]) {
  return async (req: any, res: any, next: any) => {
    const session = req.session;

    if (!session) {
      return res.status(401).json({ error: "Unauthorized" });
    }

    const user = session.user as User;
    const hasPermissions = requiredPermissions.every(
      (permission) => user.permissions.includes(permission)
    );

    if (!hasPermissions) {
      return res.status(403).json({
        error: "Forbidden: Missing required permissions",
        required: requiredPermissions,
      });
    }

    next();
  };
}

// Usage:
// app.delete("/api/users/:id",
//   requireAuth,
//   requirePermission('delete:users'),
//   handler
// );

// Multiple permissions (AND logic):
// app.post("/api/admin/users",
//   requireAuth,
//   requirePermission('write:users', 'read:users'),
//   handler
// );

Testing Protected Routes

Test your protected routes:

1. Test Without Authentication

# Should return 401 Unauthorized
curl http://localhost:3000/api/user

# Should redirect to login
curl -I http://localhost:3000/dashboard

2. Test With Authentication

# First, sign in and get the cookie
curl -c cookies.txt -L http://localhost:3000/api/auth/signin/google

# Then use the cookie for protected routes
curl -b cookies.txt http://localhost:3000/api/user

# Should return user data

3. Test Role-Based Access

# With regular user session (should fail)
curl -b cookies.txt http://localhost:3000/api/admin
# Response: 403 Forbidden

# With admin session (should succeed)
curl -b admin-cookies.txt http://localhost:3000/api/admin
# Response: 200 OK with admin content

Testing Tips

  • Use browser DevTools to inspect cookies
  • Test both successful and failed authentication scenarios
  • Verify redirect behavior for client-side routes
  • Check that 401/403 status codes are returned correctly

Best Practices

  • Always validate on the server: Client-side checks are for UX only
  • Use proper HTTP status codes: 401 for unauthenticated, 403 for unauthorized
  • Implement rate limiting: Prevent brute force attacks
  • Log authentication attempts: Monitor for suspicious activity
  • Handle edge cases: Expired tokens, deleted users, revoked permissions
  • Test thoroughly: Test all authentication and authorization paths

Security Checklist

  • Validate session on every protected request
  • Use HTTPS in production
  • Set secure cookie attributes (HttpOnly, Secure, SameSite)
  • Implement CSRF protection for state-changing operations
  • Log and monitor failed authentication attempts
  • Implement session expiration and refresh
  • Never trust client-side validation alone

Route Protection Complete

You've successfully implemented route protection with authentication middleware. Your application now securely restricts access to authenticated users only.

Next Steps

Enhance your application security:

Protected Routes Example | @warpy-auth-sdk/core