Broken Access Control w Node.js i Next.js – praktyczny przewodnik po RBAC i NextAuth

Krok po kroku pokażemy, jak zbudować solidny system kontroli dostępu w Node.js/Next.js, łącząc NextAuth z RBAC, aby wyeliminować ryzyko Broken Access Control.

Broken Access Control w Node.js i Next.js – praktyczny przewodnik po RBAC i NextAuth

Współczesne aplikacje webowe budowane w Node.js i Next.js często obsługują setki, a nawet tysiące użytkowników o różnym poziomie uprawnień. Nieodpowiednia kontrola dostępu jest jedną z najczęstszych przyczyn naruszeń bezpieczeństwa – klasyfikowaną w OWASP Top 10 jako Broken Access Control. W tym artykule przedstawiamy praktyczne podejście do budowy systemu RBAC (Role‑Based Access Control) opartego na NextAuth, które spełnia wymogi OWASP, CWE oraz zasady least‑privilege.

Model zagrożenia – co może pójść nie tak?

Podstawowy wektor ataku polega na uzyskaniu nieautoryzowanego dostępu do zasobów, które powinny być chronione. Typowe scenariusze to:

  • Użytkownik z niższym poziomem uprawnień (np. gość) uzyskuje dostęp do endpointu przeznaczonego wyłącznie dla administratorów.
  • Manipulacja parametrami w URL lub w ciele żądania pozwala obejść weryfikację uprawnień.
  • Sesja jest przechwytywana i wykorzystywana po wygaśnięciu lub po zmianie roli.

Wszystkie te przypadki są opisane w CWE‑284 (Improper Access Control) i CWE‑285 (Improper Authorization). Skuteczna obrona wymaga konsekwentnego sprawdzania uprawnień na każdym poziomie aplikacji – od warstwy routingu po warstwę danych.

Mechanizm – jak działa RBAC w NextAuth?

NextAuth zapewnia gotowy system uwierzytelniania (session, JWT) oraz hooki, które umożliwiają wstrzyknięcie własnej logiki autoryzacji. Kluczowe elementy:

  • callbacks.jwt – wzbogaca token o listę ról pobraną z bazy.
  • callbacks.session – udostępnia role w obiekcie session.user po stronie klienta.
  • Middleware (next.config.js lub dedykowany plik middleware.ts) – centralnie sprawdza, czy aktualny użytkownik ma wymagane uprawnienia do danej ścieżki.

W połączeniu z prostą tabelą ról i uprawnień w bazie (np. PostgreSQL) uzyskujemy pełny model RBAC, który można łatwo rozbudować o dodatkowe atrybuty (np. zakres organizacji).

Przykład: podatny kod vs poprawiona implementacja

Na początek przyjrzyjmy się fragmentowi, w którym brak jest weryfikacji roli – typowy błąd prowadzący do Broken Access Control.

// podatny endpoint API w Next.js (pages/api/admin/users.ts)
import { getSession } from 'next-auth/react';
export default async function handler(req, res) {
  const session = await getSession({ req });
  // jedynie sprawdzamy, czy użytkownik jest zalogowany
  if (!session) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // wykonujemy operację administracyjną
  const users = await db.user.findMany();
  res.status(200).json(users);
}

Każdy zalogowany użytkownik, niezależnie od roli, może pobrać listę wszystkich użytkowników – klasyczny przykład CWE‑285.

Poprawiona wersja wykorzystuje role zapisane w tokenie oraz centralny middleware.

// middleware.ts – globalna kontrola dostępu
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const { pathname } = req.nextUrl;

  // mapowanie ścieżek na wymagane role
  const roleMap = {
    '/api/admin': ['admin'],
    '/api/manager': ['admin', 'manager'],
  };

  for (const [prefix, roles] of Object.entries(roleMap)) {
    if (pathname.startsWith(prefix)) {
      if (!token || !roles.some(r => token.roles?.includes(r))) {
        return NextResponse.redirect(new URL('/unauthorized', req.url));
      }
    }
  }
  return NextResponse.next();
}

Teraz dostęp do /api/admin mają wyłącznie użytkownicy posiadający rolę admin. Dodatkowo, w samym handlerze możemy wykonać dodatkową walidację kontekstową.

Checklisty – co musisz sprawdzić, aby uniknąć Broken Access Control

  • Upewnij się, że token JWT zawiera listę ról i jest podpisany silnym algorytmem (HS256 lub RS256).
  • Wszystkie endpointy API i strony chronione muszą przechodzić przez middleware lub dedykowane funkcje autoryzacji.
  • Stosuj zasadę least‑privilege – przydzielaj minimalny zestaw uprawnień niezbędny do wykonania zadania.
  • Regularnie testuj uprawnienia przy pomocy narzędzi do testów penetracyjnych (np. OWASP ZAP) oraz automatycznych skanerów statycznych.
  • Loguj odmowy dostępu z informacją o brakującej roli, ale nie ujawniaj szczegółów zasobów.
„Najlepsza kontrola dostępu to taka, której nie da się ominąć, bo nie istnieje miejsce, w którym można ją pominąć.”

Typowe błędy i kompromisy w implementacji RBAC

Even experienced teams popełniają pewne pułapki:

  • Hardcoding ról w kodzie – utrudnia późniejsze rozszerzenia i prowadzi do nieścisłości między kodem a bazą danych.
  • Brak odświeżania tokenu po zmianie roli – użytkownik może zachować stare uprawnienia aż do wygaśnięcia sesji.
  • Użycie jedynie front‑endowej weryfikacji – nie chroni przed bezpośrednim wywołaniem endpointu.
  • Over‑permissive domyślne reguły – np. przyznawanie roli user do wszystkich nowych kont bez dodatkowej weryfikacji.

Rozwiązania:

  • Przechowuj definicje ról i uprawnień w centralnej tabeli, a nie w kodzie.
  • Implementuj mechanizm revocation list lub krótkie TTL tokenów, aby wymuszać odświeżanie po zmianie roli.
  • Zawsze weryfikuj uprawnienia po stronie serwera – front‑end może jedynie prezentować UI.
  • Używaj wzorca „deny‑by‑default” – brak explicitnej zgody = odmowa.

Praktyczny przykład: pełny przepływ od logowania do autoryzacji

Poniżej kompletny mini‑projekt w TypeScript, który łączy NextAuth, PostgreSQL i RBAC.

// lib/prisma.ts – klient Prisma (ORM)
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

// prisma schema (skrócone)
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  roles     Role[]   @relation("UserRoles")
}
model Role {
  id    Int    @id @default(autoincrement())
  name  String @unique
  users User[] @relation("UserRoles")
}

// pages/api/auth/[...nextauth].ts – konfiguracja NextAuth
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { prisma } from '../../../lib/prisma';

export default NextAuth({
  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
          include: { roles: true },
        });
        if (user && /* tu weryfikacja hasła */ true) {
          return { id: user.id, email: user.email, roles: user.roles.map(r => r.name) };
        }
        return null;
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.roles = user.roles; // zapamiętujemy role w JWT
      return token;
    },
    async session({ session, token }) {
      session.user.roles = token.roles as string[];
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
});

// middleware.ts – kontrola dostępu
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const url = req.nextUrl.clone();

  const protectedRoutes = [
    { prefix: '/admin', roles: ['admin'] },
    { prefix: '/manager', roles: ['admin', 'manager'] },
  ];

  for (const route of protectedRoutes) {
    if (url.pathname.startsWith(route.prefix)) {
      if (!token?.roles?.some(r => route.roles.includes(r))) {
        url.pathname = '/unauthorized';
        return NextResponse.rewrite(url);
      }
    }
  }
  return NextResponse.next();
}

Ten kod spełnia wszystkie wymienione wyżej zasady: role są przechowywane w bazie, token jest wzbogacany o role, middleware wymusza dostęp na podstawie mapy protectedRoutes, a UI może korzystać z session.user.roles do ukrywania elementów.

Podsumowanie i dalsze kroki

Implementacja solidnego systemu kontroli dostępu w Node.js i Next.js nie wymaga skomplikowanych rozwiązań – kluczowe jest konsekwentne stosowanie middleware, przechowywanie ról w bezpiecznym tokenie oraz przestrzeganie zasad least‑privilege i deny‑by‑default. Regularne przeglądy kodu, testy autoryzacji i aktualizacja dokumentacji pomagają utrzymać bezpieczeństwo na poziomie zgodnym z OWASP Top 10 i CWE‑284/285.

Jeśli potrzebujesz wsparcia przy audycie istniejących aplikacji lub chcesz zbudować od podstaw bezpieczną architekturę z wykorzystaniem NextAuth i RBAC, skontaktuj się z zespołem Coderia.it – pomożemy przekształcić Twoje rozwiązania w solidny fundament pod przyszły rozwój.

Zacznijmy

Masz projekt na oku?

Opisz go w kilku zdaniach. Odpiszę w ciągu 24 godzin z bezpłatną wyceną i propozycją stacku.