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.
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:
NextAuth.js (obecnie znany również jako Auth.js) to popularna, wszechstronna biblioteka do uwierzytelniania w aplikacjach Next.js. Jej największe zalety to:
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 };Auth.js obsługuje trzy główne strategie uwierzytelniania:
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}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}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};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}Implementując uwierzytelnianie, należy pamiętać o kilku kluczowych aspektach bezpieczeństwa:
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}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ć:
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.