Kurs Next.js · Moduł 7: Uwierzytelnianie i autoryzacja
Implementacja NextAuth.js / Auth.js
W tej lekcji14
NextAuth.js (obecnie znany również jako Auth.js) to kompleksowe rozwiązanie do uwierzytelniania w aplikacjach Next.js. Zapewnia gotową infrastrukturę do zarządzania sesjami, tokenami, logowaniem, rejestracją i integracją z zewnętrznymi dostawcami uwierzytelniania. W tym module zajmiemy się szczegółową implementacją tego narzędzia w aplikacjach Next.js.
Dlaczego warto użyć NextAuth.js?
NextAuth.js oferuje wiele korzyści:
- Gotowe rozwiązanie - nie trzeba implementować uwierzytelniania od podstaw
- Wsparcie dla różnych dostawców - oferuje integrację z ponad 50 dostawcami uwierzytelniania
- Bezpieczeństwo - implementuje najlepsze praktyki bezpieczeństwa
- Zarządzanie sesjami - obsługuje sesje po stronie serwera i klienta
- Elastyczność - można go dostosować do konkretnych potrzeb aplikacji
- Wsparcie dla App Router - działa z nową architekturą Next.js
- Rozszerzona typizacja TypeScript - oferuje pełne wsparcie dla typów
Instalacja i podstawowa konfiguracja
Zacznijmy od instalacji NextAuth.js:
1npm install next-auth@latest
2# lub
3yarn add next-auth@latest
4# lub
5pnpm add next-auth@latestKonfiguracja podstawowa dla App Router
Aby skonfigurować NextAuth.js w architekturze App Router, musimy utworzyć plik do obsługi Route API dla uwierzytelniania:
1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GithubProvider from "next-auth/providers/github";
4
5export const authOptions = {
6 providers: [
7 GithubProvider({
8 clientId: process.env.GITHUB_ID!,
9 clientSecret: process.env.GITHUB_SECRET!
10 })
11 ],
12 // Dodatkowe opcje konfiguracyjne...
13};
14
15const handler = NextAuth(authOptions);
16export { handler as GET, handler as POST };Ten plik tworzy dynamiczną trasę API, która obsługuje wszystkie niezbędne endpointy do uwierzytelniania.
Dodanie komponentu SessionProvider
Aby umożliwić korzystanie z hooka useSession w komponentach po stronie klienta, musimy dodać SessionProvider do naszej aplikacji. Najlepiej zrobić to w pliku Provider lub bezpośrednio w głównym layoucie:
1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from "next-auth/react";
5import { ReactNode } from "react";
6
7export function Providers({ children }: { children: ReactNode }) {
8 return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import { Providers } from "./providers";
13
14export default function RootLayout({
15 children,
16}: {
17 children: React.ReactNode;
18}) {
19 return (
20 <html lang="en">
21 <body>
22 <Providers>{children}</Providers>
23 </body>
24 </html>
25 );
26}Konfiguracja zmiennych środowiskowych
NextAuth.js wymaga kilku zmiennych środowiskowych. Utwórz lub zaktualizuj plik .env.local w głównym katalogu projektu:
1# Podstawowa konfiguracja dla NextAuth.js
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=twój_tajny_klucz_do_szyfrowania_jwt
4
5# Dane uwierzytelniania GitHub (przykład)
6GITHUB_ID=twoje_github_client_id
7GITHUB_SECRET=twoje_github_client_secretW środowisku produkcyjnym:
NEXTAUTH_URLpowinien być pełnym adresem URL Twojej aplikacjiNEXTAUTH_SECRETpowinien być długim, losowym ciągiem znaków. Możesz go wygenerować za pomocą komendyopenssl rand -base64 32.
Konfiguracja różnych dostawców uwierzytelniania
NextAuth.js obsługuje wiele dostawców uwierzytelniania. Przyjrzyjmy się, jak skonfigurować kilka popularnych:
1. Uwierzytelnianie z GitHub
1import GithubProvider from "next-auth/providers/github";
2
3export const authOptions = {
4 providers: [
5 GithubProvider({
6 clientId: process.env.GITHUB_ID!,
7 clientSecret: process.env.GITHUB_SECRET!,
8 // Opcjonalne parametry
9 scope: "read:user user:email", // Określa zakres dostępu
10 profile(profile) {
11 // Dostosowanie profilu użytkownika
12 return {
13 id: profile.id.toString(),
14 name: profile.name || profile.login,
15 email: profile.email,
16 image: profile.avatar_url,
17 // Dodatkowe pola
18 username: profile.login,
19 };
20 },
21 }),
22 ],
23 // Pozostałe opcje konfiguracyjne...
24};2. Uwierzytelnianie z Google
1import GoogleProvider from "next-auth/providers/google";
2
3export const authOptions = {
4 providers: [
5 GoogleProvider({
6 clientId: process.env.GOOGLE_CLIENT_ID!,
7 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
8 // Opcjonalne parametry
9 authorization: {
10 params: Promise<{
11 prompt: "consent",
12 access_type: "offline",
13 response_type: "code"
14 }>
15 }
16 }),
17 ],
18 // Pozostałe opcje konfiguracyjne...
19};3. Uwierzytelnianie z danych uwierzytelniających (email/hasło)
1import CredentialsProvider from "next-auth/providers/credentials";
2import { compare } from "bcrypt";
3import { prisma } from "@/lib/prisma"; // Zakładamy, że masz skonfigurowaną instancję Prisma
4
5export const authOptions = {
6 providers: [
7 CredentialsProvider({
8 // Nazwa wyświetlana na stronie logowania
9 name: "Credentials",
10 // Konfiguracja pól formularza logowania
11 credentials: {
12 email: { label: "Email", type: "email" },
13 password: { label: "Hasło", type: "password" }
14 },
15 async authorize(credentials) {
16 // Sprawdź, czy credentials istnieją
17 if (!credentials?.email || !credentials?.password) {
18 return null;
19 }
20
21 try {
22 // Znajdź użytkownika w bazie danych
23 const user = await prisma.user.findUnique({
24 where: { email: credentials.email },
25 });
26
27 if (!user || !user.password) {
28 return null;
29 }
30
31 // Sprawdź, czy hasło jest poprawne
32 const isPasswordValid = await compare(
33 credentials.password,
34 user.password
35 );
36
37 if (!isPasswordValid) {
38 return null;
39 }
40
41 // Zwróć obiekt użytkownika (bez hasła)
42 return {
43 id: user.id,
44 name: user.name,
45 email: user.email,
46 role: user.role,
47 // Inne pola, które chcesz uwzględnić
48 };
49 } catch (error) {
50 console.error("Authentication error:", error);
51 return null;
52 }
53 },
54 }),
55 ],
56 // Pozostałe opcje konfiguracyjne...
57};4. Uwierzytelnianie z wielu dostawców
Możesz połączyć wielu dostawców uwierzytelniania:
1export const authOptions = {
2 providers: [
3 GithubProvider({
4 clientId: process.env.GITHUB_ID!,
5 clientSecret: process.env.GITHUB_SECRET!,
6 }),
7 GoogleProvider({
8 clientId: process.env.GOOGLE_CLIENT_ID!,
9 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
10 }),
11 CredentialsProvider({
12 // Konfiguracja...
13 async authorize(credentials) {
14 // Logika uwierzytelniania...
15 },
16 }),
17 // Możesz dodać więcej dostawców...
18 ],
19 // Pozostałe opcje konfiguracyjne...
20};Konfiguracja opcji sesji i JWT
NextAuth.js oferuje różne strategie sesji:
1export const authOptions = {
2 // Konfiguracja dostawców...
3
4 session: {
5 // Wybierz strategię: 'jwt' lub 'database'
6 strategy: 'jwt',
7
8 // Maksymalny czas życia sesji (w sekundach)
9 maxAge: 30 * 24 * 60 * 60, // 30 dni
10
11 // Aktualizacja sesji przy każdym żądaniu, wydłużając jej ważność
12 updateAge: 24 * 60 * 60, // 24 godziny
13 },
14
15 // Konfiguracja JWT (tylko dla strategii 'jwt')
16 jwt: {
17 // Tajny klucz używany do podpisywania tokenów JWT
18 // Domyślnie używa NEXTAUTH_SECRET
19 secret: process.env.JWT_SECRET,
20
21 // Możesz dostosować enkoder/dekoder JWT
22 // encode: async ({ secret, token, maxAge }) => {},
23 // decode: async ({ secret, token }) => {},
24 },
25};Niestandardowe strony uwierzytelniania
Domyślnie NextAuth.js dostarcza proste strony do logowania, ale możesz je zastąpić własnymi:
1export const authOptions = {
2 // Konfiguracja dostawców...
3
4 pages: {
5 signIn: '/auth/login',
6 signOut: '/auth/logout',
7 error: '/auth/error', // Błąd uwierzytelniania (np. niewłaściwe dane)
8 verifyRequest: '/auth/verify-request', // Używane dla Magic Links
9 newUser: '/auth/new-user' // Strona po pierwszej rejestracji
10 },
11};Następnie musisz utworzyć te strony w swojej aplikacji. Oto przykład strony logowania:
1// app/auth/login/page.tsx
2'use client';
3
4import { signIn, useSession } from 'next-auth/react';
5import { useRouter, useSearchParams } from 'next/navigation';
6import { useState, useEffect } from 'react';
7
8export default function LoginPage() {
9 const router = useRouter();
10 const searchParams = useSearchParams();
11 const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
12 const { status } = useSession();
13
14 const [email, setEmail] = useState('');
15 const [password, setPassword] = useState('');
16 const [error, setError] = useState('');
17 const [isLoading, setIsLoading] = useState(false);
18
19 // Przekieruj zalogowanego użytkownika
20 useEffect(() => {
21 if (status === 'authenticated') {
22 router.push(callbackUrl);
23 }
24 }, [status, router, callbackUrl]);
25
26 // Obsługa logowania z GitHub
27 const handleGitHubSignIn = () => {
28 setIsLoading(true);
29 signIn('github', { callbackUrl });
30 };
31
32 // Obsługa logowania z Google
33 const handleGoogleSignIn = () => {
34 setIsLoading(true);
35 signIn('google', { callbackUrl });
36 };
37
38 // Obsługa logowania z danymi uwierzytelniającymi
39 const handleSubmit = async (e: React.FormEvent) => {
40 e.preventDefault();
41 setIsLoading(true);
42 setError('');
43
44 try {
45 const result = await signIn('credentials', {
46 redirect: false,
47 email,
48 password,
49 });
50
51 if (result?.error) {
52 setError('Nieprawidłowy email lub hasło');
53 setIsLoading(false);
54 } else {
55 router.push(callbackUrl);
56 }
57 } catch (error) {
58 setError('Wystąpił błąd podczas logowania');
59 setIsLoading(false);
60 }
61 };
62
63 if (status === 'loading') {
64 return <div>Ładowanie...</div>;
65 }
66
67 return (
68 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
69 <h1 className="text-2xl font-bold mb-6 text-center">Zaloguj się</h1>
70
71 {error && (
72 <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
73 {error}
74 </div>
75 )}
76
77 <div className="space-y-4 mb-6">
78 <button
79 type="button"
80 onClick={handleGitHubSignIn}
81 disabled={isLoading}
82 className="w-full flex items-center justify-center gap-2 p-2 border border-gray-300 rounded-md hover:bg-gray-50"
83 >
84 <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
85 {/* Logo GitHub */}
86 <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
87 </svg>
88 Kontynuuj z GitHub
89 </button>
90
91 <button
92 type="button"
93 onClick={handleGoogleSignIn}
94 disabled={isLoading}
95 className="w-full flex items-center justify-center gap-2 p-2 border border-gray-300 rounded-md hover:bg-gray-50"
96 >
97 <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
98 {/* Logo Google */}
99 <path d="M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm0-22.5C6.21 1.5 1.5 6.21 1.5 12S6.21 22.5 12 22.5 22.5 17.79 22.5 12 17.79 1.5 12 1.5zm-5.25 9v3h3v3h3v-3h3v-3h-3v-3h-3v3h-3z" />
100 </svg>
101 Kontynuuj z Google
102 </button>
103 </div>
104
105 <div className="relative my-6">
106 <div className="absolute inset-0 flex items-center">
107 <div className="w-full border-t border-gray-300"></div>
108 </div>
109 <div className="relative flex justify-center text-sm">
110 <span className="px-2 bg-white text-gray-500">
111 lub zaloguj się za pomocą emaila
112 </span>
113 </div>
114 </div>
115
116 <form onSubmit={handleSubmit} className="space-y-4">
117 <div>
118 <label htmlFor="email" className="block text-sm font-medium mb-1">
119 Email
120 </label>
121 <input
122 id="email"
123 type="email"
124 value={email}
125 onChange={(e) => setEmail(e.target.value)}
126 required
127 className="w-full px-3 py-2 border border-gray-300 rounded-md"
128 />
129 </div>
130
131 <div>
132 <label htmlFor="password" className="block text-sm font-medium mb-1">
133 Hasło
134 </label>
135 <input
136 id="password"
137 type="password"
138 value={password}
139 onChange={(e) => setPassword(e.target.value)}
140 required
141 className="w-full px-3 py-2 border border-gray-300 rounded-md"
142 />
143 </div>
144
145 <button
146 type="submit"
147 disabled={isLoading}
148 className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 disabled:opacity-50"
149 >
150 {isLoading ? 'Logowanie...' : 'Zaloguj się'}
151 </button>
152 </form>
153 </div>
154 );
155}Callbacki i zdarzenia
NextAuth.js umożliwia reagowanie na różne zdarzenia w cyklu życia uwierzytelniania poprzez callbacki:
1export const authOptions = {
2 // Konfiguracja dostawców...
3
4 callbacks: {
5 // Wywoływany przy tworzeniu sesji
6 async session({ session, token }) {
7 // Dodaj dane z tokenu do sesji
8 if (token.sub && session.user) {
9 session.user.id = token.sub;
10 }
11
12 // Dodaj niestandardowe pola, np. rolę użytkownika
13 if (token.role && session.user) {
14 session.user.role = token.role as string;
15 }
16
17 return session;
18 },
19
20 // Wywoływany przy tworzeniu JWT
21 async jwt({ token, user, account, profile }) {
22 // Dodaj dane użytkownika przy pierwszym zalogowaniu
23 if (user) {
24 token.role = user.role;
25 // Inne niestandardowe pola...
26 }
27
28 return token;
29 },
30
31 // Wywoływany po zalogowaniu
32 async signIn({ user, account, profile, email, credentials }) {
33 // Sprawdź, czy użytkownik ma dostęp do aplikacji
34 return true; // Zwróć false, aby odmówić dostępu
35 },
36
37 // Wywoływany podczas przekierowania
38 async redirect({ url, baseUrl }) {
39 // Zasady przekierowań
40 if (url.startsWith(baseUrl)) {
41 return url;
42 }
43 // Domyślnie przekieruj na stronę bazową
44 return baseUrl;
45 },
46 },
47
48 // Zdarzenia
49 events: {
50 async signIn(message) {
51 // Wywołane po zalogowaniu
52 console.log('User signed in:', message);
53 },
54 async signOut(message) {
55 // Wywołane po wylogowaniu
56 console.log('User signed out:', message);
57 },
58 async createUser(message) {
59 // Wywołane po utworzeniu użytkownika
60 console.log('User created:', message);
61 },
62 async linkAccount(message) {
63 // Wywołane po powiązaniu konta
64 console.log('Account linked:', message);
65 },
66 async session(message) {
67 // Wywołane po utworzeniu sesji
68 console.log('Session created/updated:', message);
69 },
70 },
71};Rozszerzanie typów dla TypeScript
Jeśli używasz TypeScript, możesz rozszerzyć typy NextAuth.js, aby uwzględnić niestandardowe pola:
1// W pliku z rozszerzeniami typów (np. types/next-auth.d.ts)
2import 'next-auth';
3import 'next-auth/jwt';
4
5declare module 'next-auth' {
6 interface Session {
7 user: {
8 id: string;
9 name?: string | null;
10 email?: string | null;
11 image?: string | null;
12 role?: string;
13 // Dodaj inne niestandardowe pola...
14 };
15 }
16
17 interface User {
18 id: string;
19 name?: string | null;
20 email?: string | null;
21 image?: string | null;
22 role?: string;
23 // Dodaj inne niestandardowe pola...
24 }
25}
26
27declare module 'next-auth/jwt' {
28 interface JWT {
29 role?: string;
30 // Dodaj inne niestandardowe pola...
31 }
32}Adaptery bazy danych
Aby używać strategii sesji opartej na bazie danych lub przechowywać konta użytkowników, NextAuth.js oferuje adaptery dla różnych baz danych:
Konfiguracja z Prisma
1import { PrismaAdapter } from "@auth/prisma-adapter";
2import { PrismaClient } from "@prisma/client";
3
4const prisma = new PrismaClient();
5
6export const authOptions = {
7 adapter: PrismaAdapter(prisma),
8 providers: [
9 // Konfiguracja dostawców...
10 ],
11 // Pozostałe opcje...
12};Schemat Prisma powinien zawierać odpowiednie modele:
1// prisma/schema.prisma
2model Account {
3 id String @id @default(cuid())
4 userId String
5 type String
6 provider String
7 providerAccountId String
8 refresh_token String? @db.Text
9 access_token String? @db.Text
10 expires_at Int?
11 token_type String?
12 scope String?
13 id_token String? @db.Text
14 session_state String?
15
16 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
17
18 @@unique([provider, providerAccountId])
19}
20
21model Session {
22 id String @id @default(cuid())
23 sessionToken String @unique
24 userId String
25 expires DateTime
26 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
27}
28
29model User {
30 id String @id @default(cuid())
31 name String?
32 email String? @unique
33 emailVerified DateTime?
34 image String?
35 password String?
36 role String? @default("user")
37 accounts Account[]
38 sessions Session[]
39}
40
41model VerificationToken {
42 identifier String
43 token String @unique
44 expires DateTime
45
46 @@unique([identifier, token])
47}Inne dostępne adaptery
NextAuth.js oferuje adaptery dla wielu baz danych:
- Adaptery SQL: PostgreSQL, MySQL, SQLite, SQL Server
- Adaptery nierelacyjne: MongoDB, DynamoDB
- ORM/ODM: Prisma, Mongoose, Typeorm, Sequelize, DrizzleORM
- Inne: Firebase, Supabase, Kysely, Planetscale, Xata
Używanie uwierzytelniania w aplikacji
1. Pobieranie danych sesji po stronie klienta
Po skonfigurowaniu NextAuth.js, możesz używać hooka useSession w komponentach klienckich:
1'use client';
2
3import { useSession } from 'next-auth/react';
4import { redirect } from 'next/navigation';
5
6export default function Dashboard() {
7 const { data: session, status } = useSession({
8 required: true,
9 onUnauthenticated() {
10 redirect('/auth/login?callbackUrl=/dashboard');
11 },
12 });
13
14 if (status === 'loading') {
15 return <div>Ładowanie...</div>;
16 }
17
18 return (
19 <div>
20 <h1>Dashboard</h1>
21 <p>Witaj, {session.user.name}!</p>
22 <p>Twój email: {session.user.email}</p>
23 {session.user.role === 'admin' && (
24 <div>
25 <h2>Panel administratora</h2>
26 {/* Zawartość tylko dla administratorów */}
27 </div>
28 )}
29 </div>
30 );
31}2. Pobieranie danych sesji po stronie serwera
W komponentach renderowanych na serwerze, możesz użyć getServerSession:
1// app/profile/page.tsx
2import { getServerSession } from 'next-auth/next';
3import { redirect } from 'next/navigation';
4import { authOptions } from '../api/auth/[...nextauth]/route';
5
6export default async function ProfilePage() {
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</h1>
16 <p>Witaj, {session.user.name}!</p>
17 <p>Twój email: {session.user.email}</p>
18 {/* Reszta zawartości profilu */}
19 </div>
20 );
21}3. Zarządzanie stanem uwierzytelniania
NextAuth.js dostarcza funkcje do zarządzania stanem uwierzytelniania:
1'use client';
2
3import { signIn, signOut, useSession } from 'next-auth/react';
4
5export function AuthButton() {
6 const { data: session } = useSession();
7
8 if (session) {
9 return (
10 <button
11 onClick={() => signOut({ callbackUrl: '/' })}
12 className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
13 >
14 Wyloguj się
15 </button>
16 );
17 }
18
19 return (
20 <button
21 onClick={() => signIn()}
22 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
23 >
24 Zaloguj się
25 </button>
26 );
27}Dostosowanie UI do NextAuth.js
Możemy stworzyć komponent, który renderuje różne elementy interfejsu w zależności od stanu uwierzytelniania:
1'use client';
2
3import { useSession } from 'next-auth/react';
4import Link from 'next/link';
5import { AuthButton } from './AuthButton';
6
7export function Navbar() {
8 const { data: session, status } = useSession();
9 const isLoading = status === 'loading';
10
11 return (
12 <nav className="bg-white shadow-md p-4">
13 <div className="container mx-auto flex justify-between items-center">
14 <Link href="/" className="text-xl font-bold">
15 MojaAplikacja
16 </Link>
17
18 <div className="flex items-center space-x-4">
19 <Link href="/" className="hover:text-blue-500">
20 Strona główna
21 </Link>
22
23 {session ? (
24 <>
25 <Link href="/dashboard" className="hover:text-blue-500">
26 Dashboard
27 </Link>
28 <Link href="/profile" className="hover:text-blue-500">
29 Profil
30 </Link>
31 {session.user.role === 'admin' && (
32 <Link href="/admin" className="hover:text-blue-500">
33 Admin
34 </Link>
35 )}
36 </>
37 ) : (
38 <Link href="/features" className="hover:text-blue-500">
39 Funkcje
40 </Link>
41 )}
42
43 {!isLoading && (
44 <div className="flex items-center space-x-2">
45 {session && (
46 <div className="flex items-center">
47 {session.user.image && (
48 <img
49 src={session.user.image}
50 alt={session.user.name || 'User'}
51 className="w-8 h-8 rounded-full mr-2"
52 />
53 )}
54 <span className="text-sm font-medium">
55 {session.user.name}
56 </span>
57 </div>
58 )}
59 <AuthButton />
60 </div>
61 )}
62 </div>
63 </div>
64 </nav>
65 );
66}Uwierzytelnianie z API Routes
NextAuth.js może być również używany do zabezpieczania endpointów API:
1// app/api/protected-data/route.ts
2import { getServerSession } from 'next-auth/next';
3import { NextResponse } from 'next/server';
4import { authOptions } from '../auth/[...nextauth]/route';
5
6export async function GET() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 return NextResponse.json(
11 { message: 'Nieautoryzowany dostęp' },
12 { status: 401 }
13 );
14 }
15
16 // Sprawdź uprawnienia na podstawie roli (opcjonalnie)
17 if (session.user.role !== 'admin') {
18 return NextResponse.json(
19 { message: 'Brak uprawnień dostępu' },
20 { status: 403 }
21 );
22 }
23
24 // Dane dostępne tylko dla zalogowanych użytkowników (i administratorów, jeśli sprawdzamy role)
25 return NextResponse.json({
26 message: 'Sukces',
27 data: {
28 secretContent: 'Te dane są chronione',
29 timestamp: new Date().toISOString()
30 }
31 });
32}Dobre praktyki przy implementacji NextAuth.js
Bezpieczeństwo przede wszystkim
- Używaj HTTPS w środowisku produkcyjnym
- Generuj silne NEXTAUTH_SECRET
- Nigdy nie przechowuj wrażliwych danych w tokenach JWT
Optymalizacja wydajności
- Wykorzystuj strategie sesji odpowiednie do twoich potrzeb
- Rozważ używanie strategii JWT dla większej skalowalności
Obsługa błędów
- Implementuj niestandardowe strony błędów
- Rejestruj błędy uwierzytelniania do analizy
Doświadczenie użytkownika
- Projektuj przyjazne formularze logowania
- Umożliwiaj płynne przełączanie między dostawcami
Testowanie
- Testuj różne scenariusze uwierzytelniania
- Sprawdzaj, jak aplikacja reaguje na wygasłe sesje
Zaawansowane scenariusze
1. Uwierzytelnianie wieloetapowe
W niektórych przypadkach możesz chcieć zaimplementować uwierzytelnianie wieloetapowe (np. hasło + kod SMS):
1// Zaimplementuj niestandardowy CredentialsProvider
2CredentialsProvider({
3 name: 'TwoFactor',
4 credentials: {
5 email: { label: "Email", type: "email" },
6 password: { label: "Hasło", type: "password" },
7 code: { label: "Kod weryfikacyjny", type: "text" }
8 },
9 async authorize(credentials) {
10 if (!credentials?.email || !credentials?.password) {
11 return null;
12 }
13
14 // Pierwszy etap: sprawdź hasło
15 const user = await prisma.user.findUnique({
16 where: { email: credentials.email }
17 });
18
19 if (!user || !await compare(credentials.password, user.password)) {
20 return null;
21 }
22
23 // Drugi etap: sprawdź kod weryfikacyjny
24 if (credentials.code) {
25 const isValidCode = await verifyTwoFactorCode(user.id, credentials.code);
26
27 if (!isValidCode) {
28 return null;
29 }
30 } else {
31 // Jeśli kod nie został podany, a jest wymagany
32 if (user.twoFactorEnabled) {
33 // Wysyłanie kodu na email/telefon
34 await sendTwoFactorCode(user);
35
36 // Zwróć specjalny obiekt, aby App Router wiedział, że potrzebujemy kodu
37 throw new Error('TwoFactorRequired');
38 }
39 }
40
41 // Uwierzytelnienie zakończone sukcesem
42 return {
43 id: user.id,
44 email: user.email,
45 name: user.name,
46 role: user.role
47 };
48 }
49})2. Implementacja własnej logiki rejestracji
1// app/api/register/route.ts
2import { NextResponse } from 'next/server';
3import { hash } from 'bcrypt';
4import { prisma } from '@/lib/prisma';
5import { z } from 'zod';
6
7// Schemat walidacji
8const userSchema = z.object({
9 name: z.string().min(2, 'Imię musi mieć co najmniej 2 znaki'),
10 email: z.string().email('Nieprawidłowy adres email'),
11 password: z.string().min(8, 'Hasło musi mieć co najmniej 8 znaków')
12});
13
14export async function POST(request: Request) {
15 try {
16 const body = await request.json();
17
18 // Walidacja danych
19 const result = userSchema.safeParse(body);
20
21 if (!result.success) {
22 return NextResponse.json(
23 { message: result.error.errors[0].message },
24 { status: 400 }
25 );
26 }
27
28 const { name, email, password } = result.data;
29
30 // Sprawdź, czy użytkownik już istnieje
31 const existingUser = await prisma.user.findUnique({
32 where: { email }
33 });
34
35 if (existingUser) {
36 return NextResponse.json(
37 { message: 'Użytkownik o podanym adresie email już istnieje' },
38 { status: 409 }
39 );
40 }
41
42 // Haszowanie hasła
43 const hashedPassword = await hash(password, 10);
44
45 // Utworzenie nowego użytkownika
46 const user = await prisma.user.create({
47 data: {
48 name,
49 email,
50 password: hashedPassword,
51 role: 'user' // Domyślna rola
52 }
53 });
54
55 // Nie zwracaj hasła w odpowiedzi
56 const { password: _, ...userWithoutPassword } = user;
57
58 return NextResponse.json(
59 { message: 'Rejestracja zakończona pomyślnie', user: userWithoutPassword },
60 { status: 201 }
61 );
62 } catch (error) {
63 console.error('Registration error:', error);
64 return NextResponse.json(
65 { message: 'Wystąpił błąd serwera' },
66 { status: 500 }
67 );
68 }
69}Podsumowanie
NextAuth.js (Auth.js) to wszechstronne narzędzie do implementacji uwierzytelniania w aplikacjach Next.js. Oferuje:
- Elastyczność - obsługuje wiele strategii uwierzytelniania
- Integrację - łatwo integruje się z różnymi dostawcami
- Bezpieczeństwo - implementuje najlepsze praktyki zabezpieczeń
- Skalowalność - działa z różnymi bazami danych i architekturami
- Rozszerzalność - można go dostosować do specyficznych potrzeb
Dzięki NextAuth.js możesz szybko zaimplementować kompletny system uwierzytelniania w swojej aplikacji Next.js, oszczędzając czas i minimalizując ryzyko błędów związanych z bezpieczeństwem.
W kolejnych modułach omówimy bardziej zaawansowane aspekty uwierzytelniania, w tym strategie uwierzytelniania, zarządzanie sesjami i tokenami, oraz implementację ról i uprawnień.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Demo: NextAuth.js / Auth.js - konfiguracja i providery
4interface AuthConfig {
5 provider: string;
6 callbackUrl: string;
7 strategy: 'jwt' | 'database';
8 maxAge: number;
9}
10
11const defaultProviders: AuthConfig[] = [
12 { provider: 'credentials', callbackUrl: '/dashboard', strategy: 'jwt', maxAge: 3600 },
13 { provider: 'google', callbackUrl: '/dashboard', strategy: 'jwt', maxAge: 86400 },
14 { provider: 'github', callbackUrl: '/dashboard', strategy: 'database', maxAge: 604800 },
15];
16
17function AuthConfigBuilder() {
18 const [configs, setConfigs] = useState<AuthConfig[]>(defaultProviders);
19 const [newProvider, setNewProvider] = useState('');
20 const [strategy, setStrategy] = useState<'jwt' | 'database'>('jwt');
21 const [showCode, setShowCode] = useState(false);
22
23 const addProvider = () => {
24 if (!newProvider.trim()) return;
25 setConfigs(prev => [...prev, {
26 provider: newProvider.trim(),
27 callbackUrl: '/dashboard',
28 strategy,
29 maxAge: strategy === 'jwt' ? 3600 : 86400,
30 }]);
31 setNewProvider('');
32 };
33
34 const removeProvider = (index: number) => {
35 setConfigs(prev => prev.filter((_, i) => i !== index));
36 };
37
38 // Słowa kluczowe ze zmiennych: podgląd edytora usuwa z kodu linie import/export, także te w tekście szablonu
39 const kw = { imp: 'import', exp: 'export' };
40 const generatedCode = `// app/api/auth/[...nextauth]/route.ts
41${kw.imp} NextAuth from 'next-auth';
42${configs.map(c => {
43 if (c.provider === 'credentials') return kw.imp + " CredentialsProvider from 'next-auth/providers/credentials';";
44 return kw.imp + " " + c.provider.charAt(0).toUpperCase() + c.provider.slice(1) + "Provider from 'next-auth/providers/" + c.provider + "';";
45}).join('\n')}
46
47${kw.exp} const authOptions = {
48 providers: [
49${configs.map(c => " // " + c.provider + " provider\n // strategy: " + c.strategy).join(',\n')}
50 ],
51 session: {
52 strategy: '${configs[0]?.strategy || 'jwt'}',
53 maxAge: ${configs[0]?.maxAge || 3600},
54 },
55 callbacks: {
56 async jwt({ token, user }) {
57 if (user) token.role = user.role;
58 return token;
59 },
60 async session({ session, token }) {
61 session.user.role = token.role;
62 return session;
63 },
64 },
65};`;
66
67 return (
68 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
69 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>NextAuth.js / Auth.js Configuration</h1>
70 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Interaktywny konfigurator NextAuth.js - dodawaj providery i generuj kod</p>
71
72 <div style={{ display: 'flex', gap: '12px', marginBottom: '24px', flexWrap: 'wrap' }}>
73 <input
74 value={newProvider}
75 onChange={e => setNewProvider(e.target.value)}
76 placeholder="Nazwa providera (np. discord)"
77 style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #333', background: '#1a1a2e', color: '#fff', flex: 1, minWidth: '200px' }}
78 />
79 <select
80 value={strategy}
81 onChange={e => setStrategy(e.target.value as 'jwt' | 'database')}
82 style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #333', background: '#1a1a2e', color: '#fff' }}
83 >
84 <option value="jwt">JWT Strategy</option>
85 <option value="database">Database Strategy</option>
86 </select>
87 <button onClick={addProvider} style={{ padding: '8px 16px', borderRadius: '6px', background: '#64ffda', color: '#0f0f23', border: 'none', cursor: 'pointer', fontWeight: 'bold' }}>
88 + Dodaj Provider
89 </button>
90 </div>
91
92 <div style={{ display: 'grid', gap: '12px', marginBottom: '24px' }}>
93 {configs.map((config, i) => (
94 <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', background: '#1a1a2e', borderRadius: '8px', border: '1px solid #333' }}>
95 <div>
96 <strong style={{ color: '#64ffda' }}>{config.provider}</strong>
97 <span style={{ color: '#8892b0', marginLeft: '12px', fontSize: '14px' }}>
98 Strategy: {config.strategy} | MaxAge: {config.maxAge}s
99 </span>
100 </div>
101 <button onClick={() => removeProvider(i)} style={{ background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer' }}>
102 Usun
103 </button>
104 </div>
105 ))}
106 </div>
107
108 <button
109 onClick={() => setShowCode(!showCode)}
110 style={{ padding: '10px 20px', borderRadius: '6px', background: showCode ? '#333' : '#7c3aed', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}
111 >
112 {showCode ? 'Ukryj kod' : 'Wygeneruj kod konfiguracji'}
113 </button>
114
115 {showCode && (
116 <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', overflow: 'auto', fontSize: '13px', lineHeight: 1.5, border: '1px solid #333' }}>
117 <code style={{ color: '#e0e0e0' }}>{generatedCode}</code>
118 </pre>
119 )}
120 </div>
121 );
122}
123
124export default AuthConfigBuilder;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Authentication oznacza:
2. Authorization oznacza:
Zadania praktyczne w grze
- Edytor kodu
Zbuduj interfejs z chronionymi sekcjami wymagającymi logowania
- Układanie w pionie
Ułóż elementy w prawidłowej kolejności: import 'server-only' → export async → fu
- Klikanie w kolejności
Ułóż elementy w prawidłowej kolejności: import → NextAuth → from
- Edytor kodu
Stwórz komponent logowania z autentykacją JWT