Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Uwierzytelnianie i Autoryzacja w Next.js

W świecie, gdzie dane osobowe i bezpieczny dostęp do zasobów są na wagę złota, uwierzytelnianie i autoryzacja stanowią fundamentalne aspekty każdej nowoczesnej aplikacji internetowej. Next.js, jako framework zorientowany na produkcyjne zastosowania, oferuje rozbudowane narzędzia i integracje umożliwiające implementację bezpiecznych systemów uwierzytelniania.

Podstawy uwierzytelniania

Uwierzytelnianie (authentication) to proces weryfikacji tożsamości użytkownika - odpowiada na pytanie "Kim jesteś?". Autoryzacja (authorization) natomiast określa, do jakich zasobów uwierzytelniony użytkownik ma dostęp - odpowiada na pytanie "Co możesz zrobić?".

W aplikacjach Next.js możemy zaimplementować uwierzytelnianie na różne sposoby:

  1. Podejście własne - budowanie systemu uwierzytelniania od podstaw
  2. Biblioteki i SDK - wykorzystanie gotowych rozwiązań jak NextAuth.js/Auth.js
  3. Zewnętrzne usługi - integracja z usługami jak Firebase Auth, Auth0, Supabase Auth

NextAuth.js / Auth.js

NextAuth.js (obecnie znany również jako Auth.js) to popularna, wszechstronna biblioteka do uwierzytelniania w aplikacjach Next.js. Jej największe zalety to:

  • Obsługa wielu dostawców uwierzytelniania (Google, GitHub, Facebook, Twitter itp.)
  • Wbudowana obsługa sesji i tokenów JWT
  • Kompatybilność z API Routes i App Router
  • Łatwa konfiguracja i minimalna ilość kodu boilerplate

Podstawowa konfiguracja Auth.js w App Router

Oto prosty przykład konfiguracji Auth.js w aplikacji z App Router:

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GitHubProvider from "next-auth/providers/github";
4import CredentialsProvider from "next-auth/providers/credentials";
5
6const handler = NextAuth({
7  providers: [
8    GitHubProvider({
9      clientId: process.env.GITHUB_ID as string,
10      clientSecret: process.env.GITHUB_SECRET as string,
11    }),
12    CredentialsProvider({
13      name: 'Credentials',
14      credentials: {
15        username: { label: "Email", type: "email" },
16        password: { label: "Hasło", type: "password" }
17      },
18      async authorize(credentials) {
19        // Tutaj należy zaimplementować własną logikę weryfikacji danych logowania
20        // np. sprawdzenie w bazie danych
21        if (credentials?.username === "admin@example.com" && credentials?.password === "password") {
22          return { id: "1", name: "Admin", email: "admin@example.com" };
23        }
24        return null;
25      }
26    }),
27  ],
28  pages: {
29    signIn: '/auth/signin',
30    signOut: '/auth/signout',
31    error: '/auth/error',
32  },
33  callbacks: {
34    async session({ session, token }) {
35      // Możemy tutaj dodać dodatkowe dane do sesji
36      return session;
37    },
38    async jwt({ token, user }) {
39      // Możemy tutaj dodać dodatkowe dane do tokenu JWT
40      if (user) {
41        token.role = user.role;
42      }
43      return token;
44    }
45  }
46});
47
48export { handler as GET, handler as POST };

Strategie uwierzytelniania

Auth.js obsługuje trzy główne strategie uwierzytelniania:

  1. OAuth - uwierzytelnianie przez dostawców zewnętrznych (Google, Facebook, GitHub itp.)
  2. Credentials - uwierzytelnianie przez własny system loginów i haseł
  3. Email - uwierzytelnianie za pomocą linków wysyłanych na email (bezhasłowe)

Użycie komponentów klienckich do zarządzania sesją

W komponentach klienckich możemy korzystać z hooków dostarczanych przez Auth.js:

1// app/components/login-button.tsx
2'use client';
3
4import { signIn, signOut, useSession } from "next-auth/react";
5
6export function LoginButton() {
7  const { data: session, status } = useSession();
8  
9  if (status === "loading") {
10    return <div>Ładowanie...</div>;
11  }
12  
13  if (session) {
14    return (
15      <div>
16        <p>Zalogowano jako: {session.user?.name}</p>
17        <button onClick={() => signOut()}>Wyloguj się</button>
18      </div>
19    );
20  }
21  
22  return <button onClick={() => signIn()}>Zaloguj się</button>;
23}

Provider sesji w układzie głównym

Aby używać useSession() w komponentach klienckich, musimy dodać SessionProvider do głównego układu aplikacji:

1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from "next-auth/react";
5
6export function Providers({ children }: { children: React.ReactNode }) {
7  return <SessionProvider>{children}</SessionProvider>;
8}
9
10// app/layout.tsx
11import { Providers } from "./providers";
12
13export default function RootLayout({ children }: { children: React.ReactNode }) {
14  return (
15    <html lang="pl">
16      <body>
17        <Providers>{children}</Providers>
18      </body>
19    </html>
20  );
21}

Middleware do ochrony tras

Next.js pozwala używać middleware do zabezpieczania tras przed nieautoryzowanym dostępem. Oto przykład middleware, który sprawdza sesję użytkownika i przekierowuje na stronę logowania, jeśli sesja nie istnieje:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import { getToken } from 'next-auth/jwt';
4import type { NextRequest } from 'next/server';
5
6export async function middleware(request: NextRequest) {
7  const token = await getToken({ req: request });
8  
9  // Ścieżki, które powinny być chronione
10  const protectedPaths = ['/dashboard', '/profile', '/settings'];
11  
12  // Sprawdzamy, czy bieżąca ścieżka jest chroniona
13  const isProtectedPath = protectedPaths.some(path => 
14    request.nextUrl.pathname.startsWith(path)
15  );
16  
17  if (isProtectedPath && !token) {
18    // Jeśli użytkownik próbuje uzyskać dostęp do chronionej ścieżki bez zalogowania,
19    // przekierowujemy go na stronę logowania
20    const loginUrl = new URL('/auth/signin', request.url);
21    loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
22    return NextResponse.redirect(loginUrl);
23  }
24  
25  return NextResponse.next();
26}
27
28export const config = {
29  matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'],
30};

Role i uprawnienia użytkowników

W bardziej złożonych aplikacjach często potrzebujemy implementować system ról i uprawnień. Możemy rozszerzyć domyślne mechanizmy Auth.js, aby obsługiwały role:

1// Rozszerzamy typy dla Auth.js
2declare module "next-auth" {
3  interface User {
4    role?: string;
5  }
6  
7  interface Session {
8    user?: {
9      id?: string;
10      name?: string;
11      email?: string;
12      role?: string;
13    };
14  }
15}
16
17// W callbackach Auth.js
18callbacks: {
19  async jwt({ token, user }) {
20    if (user) {
21      token.role = user.role;
22    }
23    return token;
24  },
25  async session({ session, token }) {
26    if (session.user) {
27      session.user.role = token.role as string;
28    }
29    return session;
30  }
31}

Następnie możemy stworzyć komponenty warunkowe bazujące na rolach:

1// app/components/role-guard.tsx
2'use client';
3
4import { useSession } from "next-auth/react";
5import { redirect } from "next/navigation";
6
7interface RoleGuardProps {
8  children: React.ReactNode;
9  allowedRoles: string[];
10}
11
12export function RoleGuard({ children, allowedRoles }: RoleGuardProps) {
13  const { data: session, status } = useSession();
14  
15  if (status === "loading") {
16    return <div>Sprawdzanie uprawnień...</div>;
17  }
18  
19  if (!session || !session.user?.role || !allowedRoles.includes(session.user.role)) {
20    return <div>Brak dostępu. Wymagane uprawnienia: {allowedRoles.join(', ')}</div>;
21  }
22  
23  return <>{children}</>;
24}

Bezpieczeństwo aplikacji

Implementując uwierzytelnianie, należy pamiętać o kilku kluczowych aspektach bezpieczeństwa:

  1. Bezpieczne przechowywanie haseł - zawsze używaj funkcji hashujących jak bcrypt, nigdy nie przechowuj haseł w postaci zwykłego tekstu
  2. HTTPS - wszystkie żądania uwierzytelniające powinny korzystać z HTTPS
  3. CSRF (Cross-Site Request Forgery) - Auth.js automatycznie zabezpiecza przed atakami CSRF
  4. Limity prób logowania - implementuj ograniczenia ilości nieudanych prób logowania
  5. Bezpieczne tokeny - upewnij się, że tokeny JWT są odpowiednio podpisane i mają rozsądny czas ważności
  6. Bezpieczne zmienne środowiskowe - tajne klucze i hasła przechowuj w zmiennych środowiskowych

Przykład implementacji limitów prób logowania

1import { NextRequest, NextResponse } from 'next/server';
2import { getToken } from 'next-auth/jwt';
3
4// Prosta implementacja w pamięci (w produkcji należałoby użyć Redis lub innego store)
5const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();
6
7export async function middleware(request: NextRequest) {
8  if (request.nextUrl.pathname === '/api/auth/callback/credentials') {
9    const ip = request.ip || 'unknown';
10    const currentAttempts = loginAttempts.get(ip) || { count: 0, lastAttempt: Date.now() };
11    
12    // Resetuj licznik po 30 minutach
13    if (Date.now() - currentAttempts.lastAttempt > 30 * 60 * 1000) {
14      currentAttempts.count = 0;
15    }
16    
17    // Jeśli przekroczono limit prób
18    if (currentAttempts.count >= 5) {
19      return new NextResponse(
20        JSON.stringify({ error: 'Zbyt wiele prób logowania. Spróbuj ponownie za 30 minut.' }),
21        { status: 429 }
22      );
23    }
24    
25    // Aktualizacja licznika
26    loginAttempts.set(ip, {
27      count: currentAttempts.count + 1,
28      lastAttempt: Date.now()
29    });
30  }
31  
32  return NextResponse.next();
33}

Podsumowanie

Uwierzytelnianie i autoryzacja to kluczowe elementy każdej aplikacji webowej. Next.js wraz z Auth.js oferuje potężne narzędzia do implementacji bezpiecznych systemów uwierzytelniania, od prostych rozwiązań po złożone systemy z rolami i uprawnieniami.

Wybierając rozwiązanie uwierzytelniania dla swojej aplikacji Next.js, warto rozważyć:

  1. Wymagania projektu - jakie metody logowania są potrzebne?
  2. Docelowi użytkownicy - czy preferują logowanie społecznościowe, czy tradycyjne?
  3. Skalę aplikacji - czy potrzebny jest złożony system ról i uprawnień?
  4. Bezpieczeństwo - jakie środki bezpieczeństwa są niezbędne dla twojej aplikacji?

Odpowiednie podejście do uwierzytelniania i autoryzacji nie tylko zabezpiecza twoją aplikację, ale także poprawia doświadczenie użytkownika, budując zaufanie do twojego produktu.

Przejdź do CodeWorlds