Cours Next.js · Module 7: Authentification et autorisation
Uwierzytelnianie i Autoryzacja w Next.js
Dans cette leçon6
In a world where personal data and secure access to resources are invaluable, authentication and authorization are fundamental aspects of any modern web application. Next.js, as a framework oriented toward production use, offers extensive tools and integrations that enable the implementation of secure authentication systems.
Podstawy authentication
Authentication is the process of verifying a user's identity - it answers the question "Who are you?". Authorization, on the other hand, determines what resources an authenticated user can access - it answers the question "What can you do?".
In Next.js applications, we can implement authentication in different ways:
- Custom approach - building an authentication system from scratch
- Libraries and SDKs - using ready-made solutions like NextAuth.js/Auth.js
- External services - integration with services like Firebase Auth, Auth0, Supabase Auth
NextAuth.js / Auth.js
NextAuth.js (now also known as Auth.js) is a popular, comprehensive library for authentication in Next.js applications. Its biggest advantages are:
- Support for multiple authentication providers (Google, GitHub, Facebook, Twitter, etc.)
- Built-in support for sessions and JWT tokens
- Compatibility with API Routes and App Router
- Easy configuration and minimal boilerplate code
Podstawowa konfiguracja Auth.js w App Router
Here is a simple Auth.js configuration example in an App Router application:
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: "Password", type: "password" }
17 },
18 async authorize(credentials) {
19 // Here you should implement your own credentials verification logic
20 // p.ex. vérification dans la base de données
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 // Here we can add additional data to the session
36 return session;
37 },
38 async jwt({ token, user }) {
39 // Here we can add additional data to the JWT token
40 if (user) {
41 token.role = user.role;
42 }
43 return token;
44 }
45 }
46});
47
48export { handler as GET, handler as POST };Strategie authentication
Auth.js supports three main authentication strategies:
- OAuth - authentication via external providers (Google, Facebook, GitHub, etc.)
- Credentials - authentication via your own login/password system
- Email - authentication using links sent by email (passwordless)
Using client components for session management
In client components we can use hooks provided by 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>Loading...</div>;
11 }
12
13 if (session) {
14 return (
15 <div>
16 <p>Zalogowano jako: {session.user?.name}</p>
17 <button onClick={() => signOut()}>Log out</button>
18 </div>
19 );
20 }
21
22 return <button onClick={() => signIn()}>Log in</button>;
23}Session Provider in the main layout
To use useSession() in client components, we need to add SessionProvider to the main application layout:
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 allows using middleware to secure routes against unauthorized access. Here is an example middleware that checks the user's session and redirects to the login page if no session exists:
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 // Paths that should be protected
10 const protectedPaths = ['/dashboard', '/profile', '/settings'];
11
12 // Check if the current path is protected
13 const isProtectedPath = protectedPaths.some(path =>
14 request.nextUrl.pathname.startsWith(path)
15 );
16
17 if (isProtectedPath && !token) {
18 // If the user tries to access a protected path without being logged in,
19 // przekierowujemy go na page 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};User roles and permissions
In more complex applications, we often need to implement a role and permission system. We can extend the default Auth.js mechanisms to support roles:
1// On étend les types pour 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}Then we can create conditional components based on roles:
1// app/components/role-guard.tsx
2'use client';
3
4import { useSession } from "next-auth/react";
5import { redirect } from "next/navigation";
6
7interface RolesGuardProps {
8 children: React.ReactNode;
9 allowedRoless: string[];
10}
11
12export function RolesGuard({ children, allowedRoless }: RolesGuardProps) {
13 const { data: session, status } = useSession();
14
15 if (status === "loading") {
16 return <div>Vérification des permissions...</div>;
17 }
18
19 if (!session || !session.user?.role || !allowedRoless.includes(session.user.role)) {
20 return <div>Access denied. Wymagane permissions: {allowedRoless.join(', ')}</div>;
21 }
22
23 return <>{children}</>;
24}Sécurité de l’application
When implementing authentication, keep in mind several key security aspects:
- Secure password storage - always use hashing functions like bcrypt, never store passwords in plain text
- HTTPS - all authentication requests should use HTTPS
- CSRF (Cross-Site Request Forgery) - Auth.js automatycznie zabezpiecza przed atakami CSRF
- Login attempt limits - implement limits on the number of failed login attempts
- Secure tokens - ensure JWT tokens are properly signed and have a reasonable validity period
- Secure environment variables - store secret keys and passwords in environment variables
Example implementation of login attempt limits
1import { NextRequest, NextResponse } from 'next/server';
2import { getToken } from 'next-auth/jwt';
3
4// Simple in-memory implementation (in production you would use Redis or another 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 // If the attempt limit has been exceeded
18 if (currentAttempts.count >= 5) {
19 return new NextResponse(
20 JSON.stringify({ error: 'Too many login attempts. Try again in 30 minutes.' }),
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}Summary
Authentication and authorization are key elements of every web application. Next.js together with Auth.js offers powerful tools for implementing secure authentication systems, from simple solutions to complex systems with roles and permissions.
When choosing an authentication solution for your Next.js application, it's worth considering:
- Project requirements - what login methods are needed?
- Target users - do they prefer social or traditional login?
- Application scale - is a complex role and permission system needed?
- Security - what security measures are necessary for your application?
A proper approach to authentication and authorization not only protects your application but also improves the user experience, building trust in your product.
Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Démo : Aperçu des méthodes d'Auth et d'Authorization dans Next.js
4interface AuthMethod {
5 id: string; name: string; icon: string; category: 'authentication' | 'authorization';
6 pros: string[]; cons: string[]; useCase: string; difficulty: string;
7 code: string;
8}
9
10const methods: AuthMethod[] = [
11 { id: 'session', name: 'Session Auth', icon: '[S]', category: 'authentication',
12 pros: ['Implémentation facile', 'Possibilité de révocation', 'Données sur le serveur'],
13 cons: ['Nécessite un storage', 'Scalabilité', 'CSRF'],
14 useCase: 'Applications web traditionnelles',
15 difficulty: 'easy',
16 code: 'const session = await getServerSession(authOptions);\nif (!session) redirect("/login");' },
17 { id: 'jwt', name: 'JWT Tokens', icon: '[J]', category: 'authentication',
18 pros: ['Stateless', 'Scalable', 'Cross-domain'],
19 cons: ['Impossible à révoquer', 'Size', 'Storage dans le navigateur'],
20 useCase: 'API, microservices, SPA',
21 difficulty: 'medium',
22 code: 'const token = req.headers.authorization?.split(" ")[1];\nconst decoded = jwt.verify(token, SECRET);' },
23 { id: 'oauth', name: 'OAuth 2.0', icon: '[O]', category: 'authentication',
24 pros: ['Pas de mots de passe en base', 'Connexion rapide', 'Fournisseurs de confiance'],
25 cons: ['Dépendance externe', 'Configuration', 'Confidentialité'],
26 useCase: 'Connexion via Google, GitHub',
27 difficulty: 'medium',
28 code: 'providers: [\n GoogleProvider({ clientId, clientSecret }),\n]' },
29 { id: 'rbac', name: 'RBAC', icon: '[R]', category: 'authorization',
30 pros: ['Modèle clair', 'Gestion facile', 'Auditabilité'],
31 cons: ['Peut être trop simple', 'Role explosion'],
32 useCase: 'Applications avec plusieurs types d\'utilisateurs',
33 difficulty: 'easy',
34 code: 'if (session.user.role !== "admin")\n return redirect("/unauthorized");' },
35 { id: 'middleware', name: 'Middleware', icon: '[M]', category: 'authorization',
36 pros: ['Centralisé', 'Edge Runtime', 'Avant le rendu'],
37 cons: ['API limitée', 'Pas d\'accès à la base de données'],
38 useCase: 'Protection des routes, redirections',
39 difficulty: 'easy',
40 code: 'export function middleware(req: NextRequest) {\n const token = req.cookies.get("token");\n if (!token) return NextResponse.redirect("/login");\n}' },
41];
42
43const diffColors: Record<string, string> = { easy: '#10b981', medium: '#f59e0b', hard: '#ef4444' };
44
45function AuthOverview() {
46 const [filter, setFilter] = useState<'all' | 'authentication' | 'authorization'>('all');
47 const [selected, setSelected] = useState<AuthMethod | null>(null);
48
49 const filtered = methods.filter(m => filter === 'all' || m.category === filter);
50
51 return (
52 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
53 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Auth & Authorization - Aperçu</h1>
54 <p style={{ color: '#8892b0', marginBottom: '20px' }}>Comparaison des méthodes d'authentification et d'autorisation</p>
55
56 <div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
57 {(['all', 'authentication', 'authorization'] as const).map(f => (
58 <button key={f} onClick={() => setFilter(f)} style={{ padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer', background: filter === f ? '#64ffda' : '#1a1a2e', color: filter === f ? '#0f0f23' : '#8892b0', fontWeight: 'bold' }}>
59 {f === 'all' ? 'Toutes' : f}
60 </button>
61 ))}
62 </div>
63
64 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '16px', marginBottom: '24px' }}>
65 {filtered.map(m => (
66 <div key={m.id} onClick={() => setSelected(m)} style={{
67 padding: '16px', borderRadius: '12px', cursor: 'pointer',
68 background: selected?.id === m.id ? '#1a2a3e' : '#1a1a2e',
69 border: selected?.id === m.id ? '1px solid #64ffda' : '1px solid #333',
70 }}>
71 <div style={{ fontSize: '20px', marginBottom: '8px' }}>{m.icon}</div>
72 <h3 style={{ color: '#64ffda', margin: '0 0 4px', fontSize: '16px' }}>{m.name}</h3>
73 <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>
74 <span style={{ fontSize: '11px', padding: '2px 6px', borderRadius: '4px', background: '#333', color: '#8892b0' }}>{m.category}</span>
75 <span style={{ fontSize: '11px', padding: '2px 6px', borderRadius: '4px', background: diffColors[m.difficulty] + '30', color: diffColors[m.difficulty] }}>{m.difficulty}</span>
76 </div>
77 <p style={{ fontSize: '13px', color: '#8892b0', margin: 0 }}>{m.useCase}</p>
78 </div>
79 ))}
80 </div>
81
82 {selected && (
83 <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333' }}>
84 <h3 style={{ color: '#64ffda', marginTop: 0 }}>{selected.icon} {selected.name}</h3>
85 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
86 <div>
87 <h4 style={{ color: '#10b981', fontSize: '14px' }}>Advantages:</h4>
88 <ul style={{ fontSize: '13px', color: '#8892b0', paddingLeft: '20px', margin: 0 }}>{selected.pros.map((p, i) => <li key={i}>{p}</li>)}</ul>
89 </div>
90 <div>
91 <h4 style={{ color: '#ef4444', fontSize: '14px' }}>Inconvénients :</h4>
92 <ul style={{ fontSize: '13px', color: '#8892b0', paddingLeft: '20px', margin: 0 }}>{selected.cons.map((c, i) => <li key={i}>{c}</li>)}</ul>
93 </div>
94 </div>
95 <pre style={{ background: '#0f0f23', padding: '12px', borderRadius: '6px', fontSize: '12px', color: '#64ffda', overflow: 'auto' }}>{selected.code}</pre>
96 </div>
97 )}
98 </div>
99 );
100}
101
102export default AuthOverview;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Parmi les méthodes d'authentification que tu as apprises, laquelle est sans état et ne nécessite pas de stocker les sessions sur le serveur ?
2. Quel mécanisme te permet de protéger les routes au niveau de l'Edge, avant que la page ne soit rendue ?
Tâches pratiques dans le jeu
- Tri vertical
Place les étapes de mise en place de l'authentification et de l'autorisation dans une application Next.js :