Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Sesje użytkowników i zarządzanie tokenami

W poprzednich modułach omówiliśmy podstawy uwierzytelniania w Next.js oraz implementację logowania z zewnętrznymi dostawcami. Teraz zagłębimy się w kluczowy aspekt każdego systemu uwierzytelniania: zarządzanie sesjami użytkowników i tokenami. Zrozumienie tych mechanizmów jest niezbędne do budowania bezpiecznych i wydajnych aplikacji internetowych.

Podstawy zarządzania sesją

Sesja użytkownika to mechanizm, który pozwala aplikacji "pamiętać" zalogowanego użytkownika podczas nawigacji między stronami i żądaniami. W nowoczesnych aplikacjach webowych sesje są implementowane na dwa główne sposoby:

  1. Sesje po stronie serwera - identyfikator sesji przechowywany w ciasteczku, a dane sesji na serwerze
  2. Sesje bezstanowe (np. JWT) - wszystkie niezbędne informacje przechowywane w tokenie, często w ciasteczku

W Next.js, a szczególnie w połączeniu z NextAuth.js, mamy dostęp do obu tych podejść.

Sesje w NextAuth.js

NextAuth.js oferuje dwie główne strategie zarządzania sesjami:

1. Strategia JWT (domyślna)

W tej strategii wszystkie informacje o sesji są przechowywane w tokenie JWT (JSON Web Token), który jest zapisywany w ciasteczku:

1// Konfiguracja strategii JWT w NextAuth.js
2export const authOptions = {
3  providers: [
4    // Konfiguracja dostawców...
5  ],
6  session: {
7    strategy: "jwt",
8    maxAge: 30 * 24 * 60 * 60, // 30 dni w sekundach
9    updateAge: 24 * 60 * 60,    // 24 godziny w sekundach
10  },
11  jwt: {
12    secret: process.env.NEXTAUTH_SECRET,
13    // Można również użyć kluczy prywatnych/publicznych zamiast prostego sekretu
14    // encryption: true, // Włącza szyfrowanie JWT
15    // signingKey: process.env.JWT_SIGNING_PRIVATE_KEY,
16    // encryptionKey: process.env.JWT_ENCRYPTION_KEY,
17  }
18};

Zalety:

  • Lepsza skalowalność - serwer nie musi przechowywać danych sesji
  • Działa dobrze w środowisku serverless
  • Prostsze w implementacji

Wady:

  • Ograniczona wielkość danych (tokeny nie powinny być zbyt duże)
  • Trudniejsze unieważnienie tokenu przed wygaśnięciem
  • Wrażliwe dane są przechowywane w tokenie (choć zaszyfrowane)

2. Strategia bazy danych

W tej strategii tylko identyfikator sesji jest przechowywany w ciasteczku, a pozostałe dane sesji są przechowywane w bazie danych:

1// Konfiguracja strategii bazy danych w NextAuth.js
2import { PrismaAdapter } from "@auth/prisma-adapter";
3import { PrismaClient } from "@prisma/client";
4
5const prisma = new PrismaClient();
6
7export const authOptions = {
8  providers: [
9    // Konfiguracja dostawców...
10  ],
11  adapter: PrismaAdapter(prisma),
12  session: {
13    strategy: "database",
14    maxAge: 30 * 24 * 60 * 60, // 30 dni w sekundach
15    updateAge: 24 * 60 * 60,    // 24 godziny w sekundach
16  }
17};

Zalety:

  • Łatwiejsze unieważnianie sesji (można usunąć wpis w bazie danych)
  • Brak ograniczeń dotyczących ilości danych
  • Bezpieczniejsze dla wrażliwych danych

Wady:

  • Wymaga dostępu do bazy danych przy każdym żądaniu
  • Nieco trudniejsza skalowalność
  • Potrzebny adapter bazy danych

Struktura tokenu JWT w NextAuth.js

Kiedy używamy strategii JWT, NextAuth.js tworzy token zawierający podstawowe informacje o użytkowniku:

1{
2  "name": "John Doe",
3  "email": "john@example.com",
4  "sub": "cl5ab1gxk0001t8kgxs4t8j1h", // identyfikator użytkownika
5  "iat": 1658309600, // czas wystawienia tokenu
6  "exp": 1660901600, // czas wygaśnięcia tokenu
7  "jti": "4b32c6a4-d2c5-4f1a-a2b3-5c84c1d94208" // unikalny identyfikator tokenu
8}

Możemy rozszerzyć token o dodatkowe dane za pomocą callbacka

jwt
:

1export const authOptions = {
2  //
3  callbacks: {
4    async jwt({ token, user, account }) {
5      // Przy pierwszym logowaniu mamy dostęp do obiektu użytkownika
6      if (user) {
7        token.role = user.role;
8        token.userId = user.id;
9        token.provider = account?.provider;
10      }
11      
12      // Możemy tu dodać logikę odświeżania tokenu
13      // np. sprawdzanie, czy token dostępu OAuth nie wygasł
14      
15      return token;
16    },
17    
18    async session({ session, token }) {
19      // Dodaj dane z tokenu do obiektu sesji (dostępne w kliencie)
20      if (session.user) {
21        session.user.role = token.role;
22        session.user.id = token.userId;
23        session.user.provider = token.provider;
24      }
25      
26      return session;
27    }
28  }
29};

Konfiguracja ciasteczek sesji

NextAuth.js domyślnie konfiguruje ciasteczka sesji zgodnie z najlepszymi praktykami bezpieczeństwa, ale możemy te ustawienia dostosować:

1export const authOptions = {
2  //
3  cookies: {
4    sessionToken: {
5      name: `__Secure-next-auth.session-token`,
6      options: {
7        httpOnly: true,
8        sameSite: "lax",
9        path: "/",
10        secure: true,  // Ustaw na false podczas lokalnego testowania bez HTTPS
11        maxAge: 30 * 24 * 60 * 60, // 30 dni w sekundach
12      },
13    },
14    // Możesz też skonfigurować inne ciasteczka używane przez NextAuth.js
15    callbackUrl: {
16      name: `__Secure-next-auth.callback-url`,
17      options: { /* ... */ },
18    },
19    csrfToken: {
20      name: `__Host-next-auth.csrf-token`,
21      options: { /* ... */ },
22    },
23  },
24};

Zarządzanie sesją po stronie klienta

W komponentach klienckich możemy używać hooka

useSession
do uzyskania dostępu do danych sesji i jej statusu:

1'use client';
2
3import { useSession } from 'next-auth/react';
4
5export default function ProfileComponent() {
6  const { data: session, status } = useSession();
7  
8  if (status === 'loading') {
9    return <p>Ładowanie...</p>;
10  }
11  
12  if (status === 'unauthenticated') {
13    return <p>Dostęp tylko dla zalogowanych użytkowników</p>;
14  }
15  
16  return (
17    <div>
18      <h1>Profil</h1>
19      <p>Witaj, {session.user.name}!</p>
20      <p>Email: {session.user.email}</p>
21      {session.user.role === 'admin' && (
22        <p>Masz uprawnienia administratora</p>
23      )}
24    </div>
25  );
26}

Możemy również wymagać sesji za pomocą opcji

required
:

1'use client';
2
3import { useSession } from 'next-auth/react';
4import { redirect } from 'next/navigation';
5
6export default function ProtectedComponent() {
7  const { data: session, status } = useSession({
8    required: true,
9    onUnauthenticated() {
10      redirect('/auth/login?callbackUrl=/profile');
11    },
12  });
13  
14  // Gdy ustawione jest required, nie musimy sprawdzać statusu 'unauthenticated'
15  if (status === 'loading') {
16    return <p>Ładowanie...</p>;
17  }
18  
19  return (
20    <div>
21      <h1>Chroniona zawartość</h1>
22      <p>Ta strona jest widoczna tylko dla zalogowanych użytkowników.</p>
23    </div>
24  );
25}

Zarządzanie sesją po stronie serwera

W komponentach serverowych lub Route Handlers możemy używać

getServerSession
do uzyskania dostępu do danych sesji:

1// Komponent serverowy (Server Component)
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export default async function ServerProfilePage() {
7  const session = await getServerSession(authOptions);
8  
9  if (!session) {
10    redirect('/auth/login?callbackUrl=/profile');
11  }
12  
13  return (
14    <div>
15      <h1>Profil (renderowany na serwerze)</h1>
16      <p>Witaj, {session.user.name}!</p>
17      <p>Email: {session.user.email}</p>
18      {/* Zawartość widoczna tylko dla zalogowanych użytkowników */}
19    </div>
20  );
21}
22
23// Route Handler (API Route)
24import { getServerSession } from "next-auth/next";
25import { NextResponse } from "next/server";
26import { authOptions } from "@/app/api/auth/[...nextauth]/route";
27
28export async function GET() {
29  const session = await getServerSession(authOptions);
30  
31  if (!session) {
32    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
33  }
34  
35  // Pobierz dane specyficzne dla zalogowanego użytkownika
36  const userData = await fetchUserData(session.user.id);
37  
38  return NextResponse.json(userData);
39}

Refresh tokeny i zarządzanie dostępem OAuth

Jeśli korzystamy z dostawców OAuth, możemy chcieć przechowywać i odświeżać tokeny dostępu, aby używać API tych dostawców:

1export const authOptions = {
2  //
3  callbacks: {
4    async jwt({ token, user, account }) {
5      // Przy pierwszym logowaniu zapisz tokeny dostępu/odświeżania
6      if (account && user) {
7        token.accessToken = account.access_token;
8        token.refreshToken = account.refresh_token;
9        token.accessTokenExpires = account.expires_at * 1000; // Konwersja na milisekundy
10        token.userId = user.id;
11        return token;
12      }
13      
14      // Jeśli token dostępu nie wygasł, zwróć go
15      if (Date.now() < token.accessTokenExpires) {
16        return token;
17      }
18      
19      // Jeśli token dostępu wygasł, odśwież go
20      return refreshAccessToken(token);
21    },
22    
23    async session({ session, token }) {
24      // Przekaż token dostępu do klienta (ale nie token odświeżania!)
25      session.accessToken = token.accessToken;
26      session.user.id = token.userId;
27      
28      return session;
29    },
30  },
31};
32
33async function refreshAccessToken(token) {
34  try {
35    // Przykład odświeżania tokenu Google OAuth
36    const url = "https://oauth2.googleapis.com/token";
37    const response = await fetch(url, {
38      headers: { "Content-Type": "application/x-www-form-urlencoded" },
39      method: "POST",
40      body: new URLSearchParams({
41        client_id: process.env.GOOGLE_CLIENT_ID,
42        client_secret: process.env.GOOGLE_CLIENT_SECRET,
43        grant_type: "refresh_token",
44        refresh_token: token.refreshToken,
45      }),
46    });
47
48    const refreshedTokens = await response.json();
49
50    if (!response.ok) {
51      throw refreshedTokens;
52    }
53
54    return {
55      ...token,
56      accessToken: refreshedTokens.access_token,
57      accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
58      // Zachowaj refresh token, chyba że otrzymaliśmy nowy
59      refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
60    };
61  } catch (error) {
62    console.error("Error refreshing access token", error);
63    // Token odświeżania mógł wygasnąć, użytkownik będzie musiał się ponownie zalogować
64    return {
65      ...token,
66      error: "RefreshAccessTokenError",
67    };
68  }
69}

Wylogowywanie i unieważnianie sesji

NextAuth.js udostępnia funkcję

signOut()
do wylogowywania:

1'use client';
2
3import { signOut } from 'next-auth/react';
4
5export function LogoutButton() {
6  return (
7    <button 
8      onClick={() => signOut({ callbackUrl: '/' })} 
9      className="bg-red-500 text-white px-4 py-2 rounded"
10    >
11      Wyloguj się
12    </button>
13  );
14}

W przypadku strategii opartej na bazie danych, możemy ręcznie unieważnić sesję, usuwając ją z bazy danych:

1// app/api/auth/invalidate-sessions/route.ts
2import { NextResponse } from "next/server";
3import { getServerSession } from "next-auth/next";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5import { prisma } from "@/lib/prisma";
6
7export async function POST(request: Request) {
8  const session = await getServerSession(authOptions);
9  
10  if (!session || session.user.role !== 'admin') {
11    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12  }
13  
14  const { userId } = await request.json();
15  
16  // Usuń wszystkie sesje użytkownika o podanym ID
17  await prisma.session.deleteMany({
18    where: {
19      userId,
20    },
21  });
22  
23  return NextResponse.json({ success: true });
24}

W przypadku strategii JWT jest to trudniejsze, ponieważ tokeny są samodzielne. Możemy jednak zaimplementować "czarną listę" tokenów lub mechanizm rotacji sekretów:

1// Implementacja czarnej listy dla tokena JWT
2const revokedTokens = new Set();
3
4export const authOptions = {
5  //
6  callbacks: {
7    async jwt({ token }) {
8      // Sprawdź, czy token jest na czarnej liście
9      if (revokedTokens.has(token.jti)) {
10        return null; // Token unieważniony
11      }
12      return token;
13    },
14  },
15  events: {
16    async signOut({ token }) {
17      // Dodaj token do czarnej listy przy wylogowaniu
18      if (token?.jti) {
19        revokedTokens.add(token.jti);
20      }
21    },
22  },
23};

To proste rozwiązanie działa tylko dla pojedynczego serwera. W przypadku wielu serwerów lub rozwiązań serverless, musielibyśmy użyć współdzielonego magazynu danych (np. Redis) do przechowywania listy unieważnionych tokenów.

Wykrywanie nieaktywności i automatyczne wylogowywanie

Możemy zaimplementować mechanizm wykrywania nieaktywności użytkownika i automatycznego wylogowywania:

1'use client';
2
3import { useSession, signOut } from 'next-auth/react';
4import { useEffect, useState } from 'react';
5
6export function InactivityDetector({ timeoutMinutes = 30 }) {
7  const { data: session } = useSession();
8  
9  useEffect(() => {
10    if (!session) return;
11    
12    let timeout;
13    const timeoutMs = timeoutMinutes * 60 * 1000;
14    
15    const resetTimeout = () => {
16      if (timeout) clearTimeout(timeout);
17      timeout = setTimeout(() => {
18        signOut({ callbackUrl: '/auth/login?timeout=true' });
19      }, timeoutMs);
20    };
21    
22    // Zresetuj timer przy każdej aktywności użytkownika
23    const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
24    
25    // Ustaw początkowy timer
26    resetTimeout();
27    
28    // Dodaj nasłuchiwanie zdarzeń
29    events.forEach(event => {
30      window.addEventListener(event, resetTimeout);
31    });
32    
33    return () => {
34      // Wyczyść timer i nasłuchiwanie przy odmontowaniu
35      if (timeout) clearTimeout(timeout);
36      events.forEach(event => {
37        window.removeEventListener(event, resetTimeout);
38      });
39    };
40  }, [session, timeoutMinutes]);
41  
42  // Komponent nie renderuje niczego widocznego
43  return null;
44}

Następnie można dodać ten komponent do głównego layoutu aplikacji:

1// app/layout.tsx
2import { Providers } from "./providers";
3import { InactivityDetector } from "@/components/InactivityDetector";
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html lang="pl">
12      <body>
13        <Providers>
14          {children}
15          <InactivityDetector timeoutMinutes={30} />
16        </Providers>
17      </body>
18    </html>
19  );
20}

Obsługa wielu urządzeń i sesji

Jeśli używamy strategii opartej na bazie danych, możemy zaimplementować zarządzanie wieloma sesjami dla tego samego użytkownika, umożliwiając mu przegląd aktywnych sesji i wylogowanie z konkretnych urządzeń:

1// app/settings/sessions/page.tsx
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5import { prisma } from "@/lib/prisma";
6import { SessionList } from "./SessionList";
7
8export default async function SessionsPage() {
9  const session = await getServerSession(authOptions);
10  
11  if (!session) {
12    redirect('/auth/login?callbackUrl=/settings/sessions');
13  }
14  
15  // Pobierz wszystkie aktywne sesje użytkownika
16  const userSessions = await prisma.session.findMany({
17    where: {
18      userId: session.user.id,
19      expires: {
20        gt: new Date(), // Tylko aktywne sesje
21      },
22    },
23    orderBy: {
24      expires: 'desc',
25    },
26  });
27  
28  // Oznacz aktualną sesję
29  const currentSessionToken = session.user.sessionToken; // Musisz dostosować, aby mieć dostęp do tokenu
30  const enhancedSessions = userSessions.map(s => ({
31    ...s,
32    isCurrent: s.sessionToken === currentSessionToken,
33  }));
34  
35  return (
36    <div className="max-w-4xl mx-auto p-6">
37      <h1 className="text-2xl font-bold mb-6">Aktywne sesje</h1>
38      <SessionList sessions={enhancedSessions} currentSessionId={session.user.id} />
39    </div>
40  );
41}
42
43// app/settings/sessions/SessionList.tsx (komponent kliencki)
44'use client';
45
46import { useState } from 'react';
47import { useRouter } from 'next/navigation';
48
49export function SessionList({ sessions, currentSessionId }) {
50  const [isLoading, setIsLoading] = useState(false);
51  const [error, setError] = useState('');
52  const router = useRouter();
53  
54  const handleRevokeSession = async (sessionToken) => {
55    if (confirm('Czy na pewno chcesz wylogować to urządzenie?')) {
56      setIsLoading(true);
57      setError('');
58      
59      try {
60        const response = await fetch('/api/auth/revoke-session', {
61          method: 'POST',
62          headers: {
63            'Content-Type': 'application/json',
64          },
65          body: JSON.stringify({ sessionToken }),
66        });
67        
68        if (!response.ok) {
69          throw new Error('Nie udało się unieważnić sesji');
70        }
71        
72        // Odśwież stronę
73        router.refresh();
74      } catch (e) {
75        setError(e.message);
76      } finally {
77        setIsLoading(false);
78      }
79    }
80  };
81  
82  const formatDate = (dateString) => {
83    return new Date(dateString).toLocaleString();
84  };
85  
86  return (
87    <div>
88      {error && (
89        <div className="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg">
90          {error}
91        </div>
92      )}
93      
94      <div className="overflow-x-auto">
95        <table className="min-w-full divide-y divide-gray-200">
96          <thead className="bg-gray-50">
97            <tr>
98              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Urządzenie</th>
99              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ostatnia aktywność</th>
100              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Wygasa</th>
101              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Akcje</th>
102            </tr>
103          </thead>
104          <tbody className="bg-white divide-y divide-gray-200">
105            {sessions.map((session) => (
106              <tr key={session.id}>
107                <td className="px-6 py-4 whitespace-nowrap">
108                  <div className="flex items-center">
109                    <div className="ml-4">
110                      <div className="text-sm font-medium text-gray-900">
111                        {session.isCurrent ? 'Aktualne urządzenie' : 'Inne urządzenie'}
112                      </div>
113                    </div>
114                  </div>
115                </td>
116                <td className="px-6 py-4 whitespace-nowrap">
117                  <div className="text-sm text-gray-900">{formatDate(session.updatedAt)}</div>
118                </td>
119                <td className="px-6 py-4 whitespace-nowrap">
120                  <div className="text-sm text-gray-900">{formatDate(session.expires)}</div>
121                </td>
122                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
123                  <button
124                    onClick={() => handleRevokeSession(session.sessionToken)}
125                    disabled={isLoading || session.isCurrent}
126                    className={`text-red-600 hover:text-red-900 ${(isLoading || session.isCurrent) ? 'opacity-50 cursor-not-allowed' : ''}`}
127                  >
128                    {session.isCurrent ? 'Aktualna sesja' : 'Wyloguj'}
129                  </button>
130                </td>
131              </tr>
132            ))}
133          </tbody>
134        </table>
135      </div>
136    </div>
137  );
138}
139
140// app/api/auth/revoke-session/route.ts
141import { NextResponse } from "next/server";
142import { getServerSession } from "next-auth/next";
143import { authOptions } from "@/app/api/auth/[...nextauth]/route";
144import { prisma } from "@/lib/prisma";
145
146export async function POST(request: Request) {
147  const session = await getServerSession(authOptions);
148  
149  if (!session) {
150    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
151  }
152  
153  const { sessionToken } = await request.json();
154  
155  // Znajdź sesję, upewniając się, że należy do zalogowanego użytkownika
156  const sessionToRevoke = await prisma.session.findFirst({
157    where: {
158      sessionToken,
159      userId: session.user.id,
160    },
161  });
162  
163  if (!sessionToRevoke) {
164    return NextResponse.json(
165      { error: "Session not found or not owned by current user" },
166      { status: 404 }
167    );
168  }
169  
170  // Usuń sesję
171  await prisma.session.delete({
172    where: {
173      id: sessionToRevoke.id,
174    },
175  });
176  
177  return NextResponse.json({ success: true });
178}

Bezpieczeństwo tokenów i sesji

Przy implementacji zarządzania sesją i tokenami, należy pamiętać o kilku kluczowych aspektach bezpieczeństwa:

  1. Używaj silnych sekretów - upewnij się, że

    NEXTAUTH_SECRET
    jest długim, losowym ciągiem znaków

  2. Zabezpiecz ciasteczka - używaj flag:

    • HttpOnly
      : zapobiega dostępowi do ciasteczka przez JavaScript
    • Secure
      : wymaga HTTPS
    • SameSite
      : chroni przed atakami CSRF
  3. Ustaw odpowiedni czas życia - tokeny/sesje nie powinny być ważne przez zbyt długi okres

  4. Ogranicz dane w tokenie - token JWT powinien zawierać minimum niezbędnych informacji

  5. Implementuj odwołanie sesji - umożliw użytkownikom wylogowanie się ze wszystkich urządzeń

  6. Monitoruj podejrzane działania - wykrywaj próby wielokrotnego logowania i nietypowe wzorce dostępu

  7. Rotacja sekretów - okresowo zmieniaj sekrety używane do podpisywania tokenów

Testy sesji i tokenów

Testy są kluczowym elementem zapewnienia niezawodności mechanizmów sesji i tokenów. Oto przykład testu jednostkowego dla funkcjonalności odświeżania tokenu:

1// __tests__/refreshToken.test.ts
2import { refreshAccessToken } from '@/lib/auth';
3import fetchMock from 'jest-fetch-mock';
4
5fetchMock.enableMocks();
6
7beforeEach(() => {
8  fetchMock.resetMocks();
9});
10
11describe('refreshAccessToken', () => {
12  it('should refresh the token successfully', async () => {
13    // Przygotuj mock odpowiedzi z serwera OAuth
14    fetchMock.mockResponseOnce(JSON.stringify({
15      access_token: 'new_access_token',
16      expires_in: 3600
17    }));
18    
19    const originalToken = {
20      accessToken: 'old_access_token',
21      refreshToken: 'refresh_token',
22      accessTokenExpires: Date.now() - 1000, // Wygasły token
23    };
24    
25    const refreshedToken = await refreshAccessToken(originalToken);
26    
27    expect(refreshedToken.accessToken).toBe('new_access_token');
28    expect(refreshedToken.refreshToken).toBe('refresh_token'); // Powinien zachować stary refresh token
29    expect(refreshedToken.accessTokenExpires).toBeGreaterThan(Date.now());
30    expect(fetchMock).toHaveBeenCalledTimes(1);
31  });
32  
33  it('should handle refresh token errors', async () => {
34    // Symuluj błąd odświeżania
35    fetchMock.mockRejectOnce(new Error('Failed to refresh token'));
36    
37    const originalToken = {
38      accessToken: 'old_access_token',
39      refreshToken: 'refresh_token',
40      accessTokenExpires: Date.now() - 1000,
41    };
42    
43    const result = await refreshAccessToken(originalToken);
44    
45    expect(result.error).toBe('RefreshAccessTokenError');
46    expect(result.accessToken).toBe('old_access_token'); // Powinien zachować stary token
47  });
48});

Dobre praktyki w kontekście Next.js App Router

W architekturze Next.js App Router, ważne jest, aby przestrzegać następujących zasad przy implementacji uwierzytelniania:

  1. Używaj middleware do ochrony całych katalogów lub grup tras:
1// middleware.ts
2import { NextResponse } from "next/server";
3import type { NextRequest } from "next/server";
4import { getToken } from "next-auth/jwt";
5
6export async function middleware(request: NextRequest) {
7  const token = await getToken({ req: request });
8  
9  // Sprawdź, czy użytkownik ma dostęp do żądanej strony
10  if (!token) {
11    const url = new URL('/auth/login', request.url);
12    url.searchParams.set('callbackUrl', request.nextUrl.pathname);
13    return NextResponse.redirect(url);
14  }
15  
16  // Sprawdzanie uprawnień na podstawie ścieżki i roli
17  if (request.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
18    return NextResponse.redirect(new URL('/dashboard', request.url));
19  }
20  
21  return NextResponse.next();
22}
23
24export const config = {
25  matcher: [
26    '/dashboard/:path*',
27    '/settings/:path*',
28    '/admin/:path*',
29    '/api/protected/:path*'
30  ],
31};
  1. Używaj Server Components gdzie to możliwe, aby zmniejszyć ilość kodu JavaScript po stronie klienta i poprawić bezpieczeństwo wrażliwych operacji:
1// app/admin/dashboard/page.tsx (Server Component)
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export default async function AdminDashboard() {
7  const session = await getServerSession(authOptions);
8  
9  if (!session || session.user.role !== 'admin') {
10    redirect('/');
11  }
12  
13  // Pobierz dane administracyjne bezpośrednio na serwerze
14  // bez narażania API keys lub innych wrażliwych informacji klientowi
15  const adminData = await fetchAdminData();
16  
17  return (
18    <div>
19      <h1>Panel administratora</h1>
20      {/* Renderuj dane administratora */}
21    </div>
22  );
23}
  1. Używaj Route Handlers do zabezpieczenia API:
1// app/api/protected/user-data/route.ts
2import { getServerSession } from "next-auth/next";
3import { NextResponse } from "next/server";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export async function GET() {
7  const session = await getServerSession(authOptions);
8  
9  if (!session) {
10    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11  }
12  
13  // Wykonaj chronione operacje
14  const userData = await fetchUserData(session.user.id);
15  
16  // Zwróć dane bez wrażliwych informacji
17  const sanitizedData = sanitizeUserData(userData);
18  
19  return NextResponse.json(sanitizedData);
20}

Podsumowanie

W tym module omówiliśmy szczegółowo, jak zarządzać sesjami użytkowników i tokenami w aplikacjach Next.js:

  1. Poznaliśmy różne strategie sesji w NextAuth.js - JWT i bazodanową
  2. Nauczyliśmy się rozszerzać tokeny i sesje o własne dane
  3. Zrozumieliśmy, jak zabezpieczać ciasteczka i konfigurować parametry sesji
  4. Poznaliśmy mechanizmy odświeżania tokenów dla dostawców OAuth
  5. Zaimplementowaliśmy zarządzanie wieloma sesjami i wylogowywanie z konkretnych urządzeń
  6. Omówiliśmy najlepsze praktyki bezpieczeństwa dla tokenów i sesji

Właściwe zarządzanie sesjami i tokenami jest kluczowym aspektem bezpieczeństwa aplikacji internetowych. Dobrze zaprojektowany system uwierzytelniania nie tylko chroni dane użytkowników, ale także zapewnia dobre doświadczenie użytkownika, umożliwiając łatwe logowanie, wylogowywanie i zarządzanie dostępem z różnych urządzeń.

W następnym module zajmiemy się implementacją systemu ról i uprawnień, który pozwoli na bardziej granularne zarządzanie dostępem użytkowników do różnych funkcji aplikacji.

Vai a CodeWorlds