AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Mongodb Rbac Middleware

skill-mrlynn-claude-skills-mongodb-rbac-middleware · by mrlynn

Add NextAuth v5 authentication with 8-role RBAC, edge-safe middleware, server-side guards, role hierarchy, and admin impersonation to a Next.js app

No reviews yet
0 installs
17 views
0.0% view→install

Install

$ agentstack add skill-mrlynn-claude-skills-mongodb-rbac-middleware

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-mrlynn-claude-skills-mongodb-rbac-middleware)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Mongodb Rbac Middleware? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

mongodb-rbac-middleware

Trigger

Use this skill when adding authentication, role-based access control, or route protection to a Next.js app. Covers NextAuth v5 with JWT strategy, edge middleware, server-side guards, role hierarchy, and admin impersonation.

> 8 roles sounds like overkill until you're building a hackathon platform with admins, organizers, judges, mentors, sponsors, and participants who all need different views of the same data. — ML

Overview

Getting auth middleware right in Next.js — especially edge middleware that can't import Mongoose — is a common stumbling block. This skill provides a battle-tested 8-role RBAC system with edge-safe JWT parsing, server-side guard functions, and an impersonation system for admin testing.

How to Use

Quick Start

Invoke with /mongodb-rbac-middleware or let Claude auto-activate when adding authentication or role-based access control.

Python Tools

  • scripts/role_matrix_generator.py — Generate role-to-route permission matrix from admin-guard config

Reference Docs

  • references/role-hierarchy.md — Full 8-role hierarchy with permissions per route
  • references/edge-runtime-constraints.md — What can/cannot run in Edge (why getToken not auth())

Templates & Samples

  • assets/role-permission-matrix.md — Pre-built role-permission matrix template
  • assets/middleware-template.ts — Starter middleware.ts for edge route protection

Architecture Decisions

  • JWT strategy over database sessions: JWTs are stateless and edge-compatible. No DB round-trip on every request.
  • getToken() in edge middleware, NOT auth(): The auth() export from NextAuth pulls in bcryptjs, mongoose, and cookies() — all Node.js-only modules that crash the Edge runtime. getToken() from next-auth/jwt is Edge-safe.
  • NextAuth v5 uses JWE, not plain JWS: Don't try to verify tokens with jose's jwtVerify — it won't work. NextAuth v5 encrypts tokens with A256CBC-HS512. Use getToken() which handles decryption.
  • Role groups, not individual role checks: Define role groups (ADMINROLES, ADMINPANEL_ROLES) and check membership. This makes adding new roles trivial.
  • Impersonation via httpOnly cookie: Admins can test as other users by setting an impersonate_user_id cookie. The session callback swaps identity but preserves realAdminRole so guards still work.

Code Patterns

Pattern 1: NextAuth v5 Configuration

// src/lib/auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import crypto from "crypto";
import { cookies } from "next/headers";
import { connectToDatabase } from "@/lib/db/connection";
import { UserModel } from "@/lib/db/models/User";
import { sendEmail } from "@/lib/email/email-service";
import { renderEmailTemplate } from "@/lib/email/template-renderer";

export const { handlers, signIn, signOut, auth } = NextAuth({
  secret: process.env.AUTH_SECRET,
  providers: [
    Credentials({
      id: "credentials",
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;

        await connectToDatabase();
        const user = await UserModel.findOne({
          email: (credentials.email as string).toLowerCase(),
        }).select("+twoFactorCode +twoFactorExpiry");

        if (!user || !user.passwordHash) return null;

        const isValid = await bcrypt.compare(credentials.password as string, user.passwordHash);
        if (!isValid) return null;

        // Check ban/soft-delete
        if (user.banned || user.deletedAt) {
          throw new Error(user.bannedReason || "Your account has been suspended.");
        }

        // 2FA flow: generate code, send email, return null to block sign-in
        if (user.twoFactorEnabled) {
          const code = Math.floor(100000 + Math.random() * 900000).toString();
          const hashedCode = crypto.createHash("sha256").update(code).digest("hex");
          user.twoFactorCode = hashedCode;
          user.twoFactorExpiry = new Date(Date.now() + 10 * 60 * 1000);
          await user.save();

          const template = await renderEmailTemplate("two_factor_code", { userName: user.name, code });
          sendEmail({ to: user.email, subject: template.subject, html: template.html, text: template.text }).catch(() => {});
          return null; // Client handles 2FA separately
        }

        return {
          id: user._id.toString(),
          email: user.email,
          name: user.name,
          role: user.role,
          partnerId: user.partnerId?.toString(),
        };
      },
    }),
    Credentials({
      id: "magic-link",
      name: "magic-link",
      credentials: {
        token: { type: "text" },
        email: { type: "email" },
      },
      async authorize(credentials) {
        if (!credentials?.token || !credentials?.email) return null;

        await connectToDatabase();
        const hashedToken = crypto.createHash("sha256").update(credentials.token as string).digest("hex");
        const user = await UserModel.findOne({
          email: (credentials.email as string).toLowerCase(),
        }).select("+magicLinkToken +magicLinkExpiry");

        if (!user?.magicLinkToken || !user?.magicLinkExpiry) return null;
        if (user.magicLinkToken !== hashedToken) return null;
        if (user.magicLinkExpiry  {
  try {
    const cookieStore = await cookies();
    return !!cookieStore.get("impersonate_user_id")?.value;
  } catch {
    return false;
  }
}

function effectiveRole(user: { role?: string; realAdminRole?: string; isImpersonating?: boolean }): UserRole {
  return ((user.isImpersonating ? user.realAdminRole : user.role) ?? user.role ?? "") as UserRole;
}

export async function requireAdmin() {
  const session = await auth();
  if (!session?.user) redirect("/login");
  const role = effectiveRole(session.user as { role?: string; realAdminRole?: string; isImpersonating?: boolean });
  if (!ADMIN_ROLES.includes(role) && !(await isImpersonationActive())) redirect("/dashboard");
  return session;
}

export async function requireAdminPanel() {
  const session = await auth();
  if (!session?.user) redirect("/login");
  const role = effectiveRole(session.user as { role?: string; realAdminRole?: string; isImpersonating?: boolean });
  if (!ADMIN_PANEL_ROLES.includes(role) && !(await isImpersonationActive())) redirect("/dashboard");
  return session;
}

export async function requireSuperAdmin() {
  const session = await auth();
  if (!session?.user) redirect("/login");
  const role = effectiveRole(session.user as { role?: string; realAdminRole?: string; isImpersonating?: boolean });
  if (role !== "super_admin") redirect("/admin");
  return session;
}

export async function requirePartner() {
  const session = await auth();
  if (!session?.user) redirect("/login");
  const role = effectiveRole(session.user as { role?: string; realAdminRole?: string; isImpersonating?: boolean });
  if (!PARTNER_PORTAL_ROLES.includes(role) && !(await isImpersonationActive())) redirect("/dashboard");
  return session;
}

export async function hasRole(...roles: UserRole[]): Promise {
  const session = await auth();
  if (!session?.user) return false;
  const role = effectiveRole(session.user as { role?: string; realAdminRole?: string; isImpersonating?: boolean });
  return roles.includes(role);
}

export async function isUserAdmin(): Promise {
  const session = await auth();
  if (!session?.user) return false;
  const role = effectiveRole(session.user as { role?: string; realAdminRole?: string; isImpersonating?: boolean });
  if (ADMIN_ROLES.includes(role)) return true;
  return isImpersonationActive();
}

Pattern 4: Edge Middleware (CRITICAL)

This is the pattern most people get wrong. You CANNOT import auth from @/lib/auth in middleware because it transitively imports bcryptjs, mongoose, and cookies() — all of which crash the Edge runtime.

// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";

/**
 * CRITICAL: We use getToken(), NOT auth().
 * auth() imports bcryptjs + mongoose + cookies() = Edge crash.
 * getToken() is Edge-safe and handles NextAuth v5 JWE decryption.
 */

interface TokenPayload {
  role?: string;
  sub?: string;
}

async function getTokenPayload(req: NextRequest): Promise {
  try {
    const token = await getToken({ req, secret: process.env.AUTH_SECRET! });
    if (!token) return null;
    return { role: (token.role as string) || undefined, sub: token.sub || undefined };
  } catch {
    return null;
  }
}

export async function middleware(req: NextRequest) {
  const { pathname, searchParams } = req.nextUrl;

  // Admin route protection
  if (pathname.startsWith("/admin")) {
    const payload = await getTokenPayload(req);
    if (!payload) {
      const loginUrl = new URL("/login", req.url);
      loginUrl.searchParams.set("callbackUrl", req.url);
      return NextResponse.redirect(loginUrl);
    }
    const role = payload.role;
    if (role !== "admin" && role !== "super_admin" && role !== "organizer" && role !== "marketer") {
      return NextResponse.redirect(new URL("/dashboard", req.url));
    }
    return NextResponse.next();
  }

  // Judging route protection
  if (pathname.startsWith("/judging")) {
    const payload = await getTokenPayload(req);
    if (!payload) {
      const loginUrl = new URL("/login", req.url);
      loginUrl.searchParams.set("callbackUrl", req.url);
      return NextResponse.redirect(loginUrl);
    }
    const role = payload.role;
    if (role !== "judge" && role !== "admin" && role !== "super_admin" && role !== "organizer") {
      return NextResponse.redirect(new URL("/dashboard", req.url));
    }
    return NextResponse.next();
  }

  // Partner portal protection (exclude /partner/register)
  if (pathname.startsWith("/partner") && !pathname.startsWith("/partner/register")) {
    const payload = await getTokenPayload(req);
    if (!payload) {
      const loginUrl = new URL("/login", req.url);
      loginUrl.searchParams.set("callbackUrl", req.url);
      return NextResponse.redirect(loginUrl);
    }
    const role = payload.role;
    if (role !== "partner" && role !== "admin" && role !== "super_admin") {
      return NextResponse.redirect(new URL("/dashboard", req.url));
    }
    return NextResponse.next();
  }

  // Landing page preview mode guard
  if (searchParams.get("preview")) {
    const payload = await getTokenPayload(req);
    const role = payload?.role;
    if (role === "admin" || role === "super_admin" || role === "organizer" || role === "marketer") {
      return NextResponse.next();
    }
    const loginUrl = new URL("/login", req.url);
    loginUrl.searchParams.set("callbackUrl", req.url);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    "/admin/:path*",
    "/judging/:path*",
    "/partner/:path*",
    "/((?!api|_next/static|_next/image|favicon\\.ico|login|register|admin|dashboard|events|judging|profile|projects|settings).*)",
  ],
};

Pattern 5: Using Guards in API Routes

// In route handlers that need admin access:
import { auth } from "@/lib/auth";
import { errorResponse } from "@/lib/utils";

export async function POST(request: NextRequest) {
  const session = await auth();
  const role = (session?.user as { role?: string })?.role;
  if (!role || !["admin", "super_admin"].includes(role)) {
    return errorResponse("Forbidden", 403);
  }
  // ... proceed
}

// In server components / layouts that need admin access:
import { requireAdminPanel } from "@/lib/admin-guard";
import type { UserRole } from "@/lib/admin-guard";

export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await requireAdminPanel(); // Redirects if unauthorized
  const userRole = ((session.user as { role?: string }).role ?? "participant") as UserRole;
  return {children};
}

Pattern 6: User Model with Auth Fields

// Key auth-related fields on the User model (see mongodb-nextjs-scaffold for full model)
const UserSchema = new Schema({
  email: { type: String, required: true, unique: true, lowercase: true },
  passwordHash: { type: String },                    // bcrypt hash
  magicLinkToken: { type: String, select: false },   // SHA-256 hash (never return in queries)
  magicLinkExpiry: { type: Date, select: false },
  twoFactorEnabled: { type: Boolean, default: false },
  twoFactorCode: { type: String, select: false },    // SHA-256 hash
  twoFactorExpiry: { type: Date, select: false },
  role: {
    type: String,
    enum: ["super_admin", "admin", "organizer", "marketer", "mentor", "judge", "participant", "partner"],
    default: "participant",
  },
  banned: { type: Boolean, default: false },
  bannedAt: { type: Date },
  bannedReason: { type: String },
  deletedAt: { type: Date },                         // Soft delete
}, { timestamps: true });

Environment Variables

AUTH_SECRET=your-random-secret-min-32-chars   # Required for JWT encryption
NEXTAUTH_URL=http://localhost:3000            # Required for callbacks

Dependencies

npm install next-auth@beta bcryptjs
npm install -D @types/bcryptjs

Common Pitfalls

  • NEVER import @/lib/auth in middleware.ts. It pulls in bcryptjs/mongoose which crash Edge. Use getToken from next-auth/jwt.
  • NEVER use jose jwtVerify to decode NextAuth v5 tokens. They're JWE-encrypted (A256CBC-HS512), not plain JWS. Only getToken handles this correctly.
  • Always hash tokens before storing. Magic link tokens and 2FA codes are hashed with SHA-256 before saving to the database. Compare hashes, never raw values.
  • Use select: false for sensitive fields. Password hashes, tokens, and 2FA codes should never be returned in normal queries. Use .select("+fieldName") when you explicitly need them.
  • Soft-delete users, don't hard-delete. Set deletedAt and check for it in queries. This preserves audit trails.
  • Fire-and-forget for 2FA emails. Use .catch(() => {}) — don't let email failures block the auth flow.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.