RAICODE
ProcessusProjetsBlogOffresClientsContact
development

Sécuriser une Application Next.js : Authentification et Bonnes Pratiques

Guide complet pour sécuriser votre application Next.js : NextAuth.js, gestion des sessions, protection des routes API, middleware de sécurité et bonnes pratiques OWASP.

Mustapha Hamadi
Développeur Full-Stack
7 décembre 2025
15 min read
Sécuriser une Application Next.js : Authentification et Bonnes Pratiques
#Sécurité#Next.js#Authentification
Partager :

title: "Sécuriser une Application Next.js : Authentification et Bonnes Pratiques" description: "Guide complet pour sécuriser votre application Next.js : NextAuth.js, gestion des sessions, protection des routes API, middleware de sécurité et bonnes pratiques OWASP." date: "2025-12-07" author: name: "Mustapha Hamadi" role: "Développeur Full-Stack" image: "/avatar.jpg" tags: ["Sécurité", "Next.js", "Authentification"] category: "development" image: "/blog/securiser-application-nextjs-authentification-guide-hero.png" ogImage: "/blog/securiser-application-nextjs-authentification-guide-hero.png" featured: false published: true keywords: ["NextAuth", "authentification", "sécurité web", "JWT", "protection API", "middleware Next.js", "sessions", "OWASP", "CSRF", "XSS", "OAuth", "2FA", "bcrypt", "rate limiting"]

Sécuriser une Application Next.js : Authentification et Bonnes Pratiques

La sécurité n'est pas une fonctionnalité optionnelle. Une faille peut compromettre les données de vos utilisateurs et ruiner la réputation de votre entreprise. Ce guide couvre les aspects essentiels de la sécurisation d'une application Next.js, de l'authentification aux bonnes pratiques OWASP.

Architecture de Sécurité Next.js

Vue d'Ensemble

┌─────────────────────────────────────────────────────────┐
│                      Client                             │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                    Middleware                           │
│  - Rate limiting                                        │
│  - Authentication check                                 │
│  - Security headers                                     │
└─────────────────────────────────────────────────────────┘
                           │
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
    │   Pages     │ │ API Routes  │ │   Server    │
    │  (Public)   │ │ (Protected) │ │  Actions    │
    └─────────────┘ └─────────────┘ └─────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│                    Database                             │
│  - Encrypted passwords                                  │
│  - Prepared statements                                  │
└─────────────────────────────────────────────────────────┘

Authentification avec NextAuth.js

Installation et Configuration

npm install next-auth @auth/prisma-adapter
npm install bcryptjs
npm install -D @types/bcryptjs

Configuration de Base

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
// lib/auth.ts
import { NextAuthOptions } from 'next-auth';
import { PrismaAdapter } from '@auth/prisma-adapter';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 jours
  },
  pages: {
    signIn: '/login',
    signOut: '/logout',
    error: '/auth/error',
    newUser: '/onboarding',
  },
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Mot de passe', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Email et mot de passe requis');
        }

        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
        });

        if (!user || !user.hashedPassword) {
          throw new Error('Identifiants invalides');
        }

        const isValid = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        );

        if (!isValid) {
          throw new Error('Identifiants invalides');
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
        };
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user, trigger, session }) {
      // Première connexion : ajouter les infos utilisateur
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }

      // Mise à jour de session demandée
      if (trigger === 'update' && session) {
        token.name = session.name;
      }

      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
      }
      return session;
    },
  },
  events: {
    async signIn({ user, isNewUser }) {
      // Logger les connexions pour l'audit
      await prisma.auditLog.create({
        data: {
          action: 'SIGN_IN',
          userId: user.id,
          metadata: { isNewUser },
        },
      });
    },
  },
};

Types TypeScript Étendus

// types/next-auth.d.ts
import { DefaultSession, DefaultUser } from 'next-auth';
import { JWT, DefaultJWT } from 'next-auth/jwt';

declare module 'next-auth' {
  interface Session {
    user: {
      id: string;
      role: string;
    } & DefaultSession['user'];
  }

  interface User extends DefaultUser {
    role: string;
  }
}

declare module 'next-auth/jwt' {
  interface JWT extends DefaultJWT {
    id: string;
    role: string;
  }
}

Inscription Sécurisée

// app/api/auth/register/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
import { z } from 'zod';

const registerSchema = z.object({
  name: z.string().min(2, 'Nom trop court').max(50),
  email: z.string().email('Email invalide'),
  password: z
    .string()
    .min(8, 'Minimum 8 caractères')
    .regex(/[A-Z]/, 'Au moins une majuscule')
    .regex(/[a-z]/, 'Au moins une minuscule')
    .regex(/[0-9]/, 'Au moins un chiffre')
    .regex(/[^A-Za-z0-9]/, 'Au moins un caractère spécial'),
});

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const { name, email, password } = registerSchema.parse(body);

    // Vérifier si l'email existe déjà
    const existingUser = await prisma.user.findUnique({
      where: { email },
    });

    if (existingUser) {
      // Message générique pour éviter l'énumération
      return NextResponse.json(
        { error: 'Impossible de créer le compte' },
        { status: 400 }
      );
    }

    // Hasher le mot de passe (coût 12 recommandé)
    const hashedPassword = await bcrypt.hash(password, 12);

    // Créer l'utilisateur
    const user = await prisma.user.create({
      data: {
        name,
        email,
        hashedPassword,
        role: 'USER',
      },
      select: {
        id: true,
        name: true,
        email: true,
      },
    });

    return NextResponse.json(user, { status: 201 });

  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Données invalides', details: error.errors },
        { status: 400 }
      );
    }

    console.error('Erreur inscription:', error);
    return NextResponse.json(
      { error: 'Erreur interne' },
      { status: 500 }
    );
  }
}

Protection des Routes avec Middleware

Middleware de Sécurité Complet

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

// Routes protégées
const protectedRoutes = ['/dashboard', '/settings', '/profile', '/admin'];
const adminRoutes = ['/admin'];
const authRoutes = ['/login', '/register'];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Headers de sécurité
  const response = NextResponse.next();
  setSecurityHeaders(response);

  // 2. Rate limiting basique
  const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'unknown';
  if (isRateLimited(ip, pathname)) {
    return new NextResponse('Too Many Requests', { status: 429 });
  }

  // 3. Vérification authentification
  const token = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  const isAuthenticated = !!token;
  const isProtectedRoute = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  );
  const isAdminRoute = adminRoutes.some((route) =>
    pathname.startsWith(route)
  );
  const isAuthRoute = authRoutes.some((route) =>
    pathname.startsWith(route)
  );

  // Redirection si non authentifié sur route protégée
  if (isProtectedRoute && !isAuthenticated) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Vérification rôle admin
  if (isAdminRoute && token?.role !== 'ADMIN') {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }

  // Redirection si déjà connecté sur page auth
  if (isAuthRoute && isAuthenticated) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return response;
}

// Headers de sécurité
function setSecurityHeaders(response: NextResponse) {
  // Empêcher le clickjacking
  response.headers.set('X-Frame-Options', 'DENY');

  // Empêcher le sniffing MIME
  response.headers.set('X-Content-Type-Options', 'nosniff');

  // Contrôler le referrer
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions Policy
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );

  // Content Security Policy
  response.headers.set(
    'Content-Security-Policy',
    `
      default-src 'self';
      script-src 'self' 'unsafe-eval' 'unsafe-inline';
      style-src 'self' 'unsafe-inline';
      img-src 'self' blob: data: https:;
      font-src 'self';
      object-src 'none';
      base-uri 'self';
      form-action 'self';
      frame-ancestors 'none';
      upgrade-insecure-requests;
    `.replace(/\s+/g, ' ').trim()
  );
}

// Rate limiting simple (en production, utilisez Redis)
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();

function isRateLimited(ip: string, pathname: string): boolean {
  const key = `${ip}:${pathname}`;
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxRequests = pathname.startsWith('/api') ? 60 : 200;

  const current = rateLimitMap.get(key);

  if (!current || now > current.resetAt) {
    rateLimitMap.set(key, { count: 1, resetAt: now + windowMs });
    return false;
  }

  if (current.count >= maxRequests) {
    return true;
  }

  current.count++;
  return false;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|public/).*)',
  ],
};

Protection des API Routes

Wrapper de Protection

// lib/api-auth.ts
import { getServerSession } from 'next-auth';
import { NextRequest, NextResponse } from 'next/server';
import { authOptions } from './auth';

type Role = 'USER' | 'ADMIN' | 'EDITOR';

interface AuthOptions {
  requiredRole?: Role | Role[];
}

export function withAuth(
  handler: (
    req: NextRequest,
    context: { params: Record<string, string>; session: Session }
  ) => Promise<NextResponse>,
  options: AuthOptions = {}
) {
  return async (
    req: NextRequest,
    context: { params: Record<string, string> }
  ) => {
    const session = await getServerSession(authOptions);

    if (!session) {
      return NextResponse.json(
        { error: 'Non authentifié' },
        { status: 401 }
      );
    }

    // Vérification du rôle si requis
    if (options.requiredRole) {
      const requiredRoles = Array.isArray(options.requiredRole)
        ? options.requiredRole
        : [options.requiredRole];

      if (!requiredRoles.includes(session.user.role as Role)) {
        return NextResponse.json(
          { error: 'Accès non autorisé' },
          { status: 403 }
        );
      }
    }

    return handler(req, { ...context, session });
  };
}

// Utilisation
// app/api/admin/users/route.ts
import { withAuth } from '@/lib/api-auth';

export const GET = withAuth(
  async (req, { session }) => {
    const users = await prisma.user.findMany({
      select: { id: true, name: true, email: true, role: true },
    });

    return NextResponse.json(users);
  },
  { requiredRole: 'ADMIN' }
);

Validation des Entrées

// lib/validation.ts
import { z } from 'zod';
import { NextRequest, NextResponse } from 'next/server';

export function validateRequest<T extends z.ZodSchema>(schema: T) {
  return async (req: NextRequest): Promise<z.infer<T> | NextResponse> => {
    try {
      const body = await req.json();
      return schema.parse(body);
    } catch (error) {
      if (error instanceof z.ZodError) {
        return NextResponse.json(
          {
            error: 'Données invalides',
            details: error.errors.map((e) => ({
              field: e.path.join('.'),
              message: e.message,
            })),
          },
          { status: 400 }
        );
      }
      throw error;
    }
  };
}

// Utilisation
const updateUserSchema = z.object({
  name: z.string().min(2).max(50).optional(),
  email: z.string().email().optional(),
});

export async function PUT(req: NextRequest) {
  const result = await validateRequest(updateUserSchema)(req);

  if (result instanceof NextResponse) {
    return result; // Erreur de validation
  }

  const data = result; // Données validées
  // ...
}

Protection CSRF

Token CSRF avec Server Actions

// lib/csrf.ts
import { cookies } from 'next/headers';
import crypto from 'crypto';

const CSRF_SECRET = process.env.CSRF_SECRET!;
const CSRF_COOKIE = 'csrf-token';

export function generateCsrfToken(): string {
  const token = crypto.randomBytes(32).toString('hex');
  const signature = crypto
    .createHmac('sha256', CSRF_SECRET)
    .update(token)
    .digest('hex');

  return `${token}.${signature}`;
}

export function verifyCsrfToken(token: string): boolean {
  const [value, signature] = token.split('.');

  if (!value || !signature) {
    return false;
  }

  const expectedSignature = crypto
    .createHmac('sha256', CSRF_SECRET)
    .update(value)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Middleware pour les formulaires
export async function csrfProtection(formData: FormData) {
  const csrfToken = formData.get('csrf_token') as string;
  const cookieStore = cookies();
  const storedToken = cookieStore.get(CSRF_COOKIE)?.value;

  if (!csrfToken || !storedToken || csrfToken !== storedToken) {
    throw new Error('Token CSRF invalide');
  }

  if (!verifyCsrfToken(csrfToken)) {
    throw new Error('Token CSRF invalide');
  }
}

Composant de Formulaire Sécurisé

// components/secure-form.tsx
'use client';

import { useEffect, useState } from 'react';

interface SecureFormProps extends React.FormHTMLAttributes<HTMLFormElement> {
  children: React.ReactNode;
}

export function SecureForm({ children, ...props }: SecureFormProps) {
  const [csrfToken, setCsrfToken] = useState('');

  useEffect(() => {
    // Récupérer le token CSRF depuis l'API
    fetch('/api/csrf')
      .then((res) => res.json())
      .then((data) => setCsrfToken(data.token));
  }, []);

  return (
    <form {...props}>
      <input type="hidden" name="csrf_token" value={csrfToken} />
      {children}
    </form>
  );
}

Protection XSS

Sanitization des Entrées

// lib/sanitize.ts
import DOMPurify from 'isomorphic-dompurify';

// Sanitizer pour le HTML
export function sanitizeHtml(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
  });
}

// Sanitizer pour le texte brut
export function sanitizeText(input: string): string {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// Validation d'URL
export function isValidUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

Affichage Sécurisé du Contenu Utilisateur

// components/user-content.tsx
import { sanitizeHtml, sanitizeText } from '@/lib/sanitize';

interface UserContentProps {
  content: string;
  allowHtml?: boolean;
}

export function UserContent({ content, allowHtml = false }: UserContentProps) {
  if (allowHtml) {
    // Contenu HTML sanitizé
    return (
      <div
        dangerouslySetInnerHTML={{ __html: sanitizeHtml(content) }}
        className="prose"
      />
    );
  }

  // Texte brut - React échappe automatiquement
  return <p>{content}</p>;
}

// Mauvaise pratique - À éviter
// <div dangerouslySetInnerHTML={{ __html: userInput }} />

Protection SQL Injection

Requêtes Prisma (Sécurisées par Défaut)

// Prisma utilise des requêtes préparées - sécurisé par défaut
const user = await prisma.user.findUnique({
  where: { email: userInput }, // Automatiquement échappé
});

// DANGER : Raw queries sans préparation
// const users = await prisma.$queryRawUnsafe(
//   `SELECT * FROM users WHERE email = '${userInput}'` // VULNÉRABLE
// );

// CORRECT : Raw queries avec paramètres
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE email = ${userInput}
`;

Validation des IDs

// lib/validators.ts
import { z } from 'zod';

// Schéma pour les UUIDs
export const uuidSchema = z.string().uuid();

// Schéma pour les IDs numériques
export const numericIdSchema = z.coerce.number().int().positive();

// Utilisation dans les routes
export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const idResult = uuidSchema.safeParse(params.id);

  if (!idResult.success) {
    return NextResponse.json(
      { error: 'ID invalide' },
      { status: 400 }
    );
  }

  const user = await prisma.user.findUnique({
    where: { id: idResult.data },
  });

  // ...
}

Gestion Sécurisée des Sessions

Configuration des Cookies

// lib/auth.ts
export const authOptions: NextAuthOptions = {
  // ...
  cookies: {
    sessionToken: {
      name: process.env.NODE_ENV === 'production'
        ? '__Secure-next-auth.session-token'
        : 'next-auth.session-token',
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: process.env.NODE_ENV === 'production',
        maxAge: 30 * 24 * 60 * 60, // 30 jours
      },
    },
    csrfToken: {
      name: process.env.NODE_ENV === 'production'
        ? '__Host-next-auth.csrf-token'
        : 'next-auth.csrf-token',
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: process.env.NODE_ENV === 'production',
      },
    },
  },
};

Rotation des Tokens

// lib/auth.ts
export const authOptions: NextAuthOptions = {
  // ...
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
        token.issuedAt = Date.now();
      }

      // Rotation du token après 1 heure
      const tokenAge = Date.now() - (token.issuedAt as number);
      if (tokenAge > 3600000) {
        token.issuedAt = Date.now();
        // Le token sera automatiquement re-signé
      }

      return token;
    },
  },
};

Authentification à Deux Facteurs (2FA)

Implémentation TOTP

// lib/2fa.ts
import { authenticator } from 'otplib';
import QRCode from 'qrcode';

export function generateSecret(email: string) {
  const secret = authenticator.generateSecret();
  const otpauth = authenticator.keyuri(email, 'MonApp', secret);

  return { secret, otpauth };
}

export async function generateQRCode(otpauth: string): Promise<string> {
  return QRCode.toDataURL(otpauth);
}

export function verifyToken(token: string, secret: string): boolean {
  return authenticator.verify({ token, secret });
}
// app/api/2fa/setup/route.ts
import { getServerSession } from 'next-auth';
import { NextResponse } from 'next/server';
import { generateSecret, generateQRCode } from '@/lib/2fa';
import { prisma } from '@/lib/prisma';

export async function POST() {
  const session = await getServerSession(authOptions);

  if (!session) {
    return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
  }

  const { secret, otpauth } = generateSecret(session.user.email!);
  const qrCode = await generateQRCode(otpauth);

  // Stocker le secret temporairement (en attente de vérification)
  await prisma.user.update({
    where: { id: session.user.id },
    data: { pendingTwoFactorSecret: secret },
  });

  return NextResponse.json({ qrCode, secret });
}
// app/api/2fa/verify/route.ts
import { verifyToken } from '@/lib/2fa';

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  const { token } = await req.json();

  const user = await prisma.user.findUnique({
    where: { id: session!.user.id },
  });

  if (!user?.pendingTwoFactorSecret) {
    return NextResponse.json(
      { error: '2FA non initialisé' },
      { status: 400 }
    );
  }

  const isValid = verifyToken(token, user.pendingTwoFactorSecret);

  if (!isValid) {
    return NextResponse.json(
      { error: 'Code invalide' },
      { status: 400 }
    );
  }

  // Activer le 2FA
  await prisma.user.update({
    where: { id: user.id },
    data: {
      twoFactorSecret: user.pendingTwoFactorSecret,
      pendingTwoFactorSecret: null,
      twoFactorEnabled: true,
    },
  });

  return NextResponse.json({ success: true });
}

Audit et Logging

Logger de Sécurité

// lib/audit-logger.ts
import { prisma } from './prisma';

type AuditAction =
  | 'SIGN_IN'
  | 'SIGN_OUT'
  | 'SIGN_IN_FAILED'
  | 'PASSWORD_CHANGED'
  | 'TWO_FACTOR_ENABLED'
  | 'TWO_FACTOR_DISABLED'
  | 'PERMISSION_DENIED'
  | 'DATA_ACCESSED'
  | 'DATA_MODIFIED';

interface AuditLogData {
  action: AuditAction;
  userId?: string;
  ip?: string;
  userAgent?: string;
  metadata?: Record<string, unknown>;
}

export async function logAuditEvent(data: AuditLogData) {
  try {
    await prisma.auditLog.create({
      data: {
        action: data.action,
        userId: data.userId,
        ip: data.ip,
        userAgent: data.userAgent,
        metadata: data.metadata ?? {},
        timestamp: new Date(),
      },
    });
  } catch (error) {
    // Logger en fallback si la DB échoue
    console.error('Audit log failed:', data, error);
  }
}

// Utilisation dans les routes
export async function POST(req: NextRequest) {
  const ip = req.headers.get('x-forwarded-for') ?? 'unknown';
  const userAgent = req.headers.get('user-agent') ?? 'unknown';

  try {
    // ... logique d'authentification

    await logAuditEvent({
      action: 'SIGN_IN',
      userId: user.id,
      ip,
      userAgent,
    });

  } catch (error) {
    await logAuditEvent({
      action: 'SIGN_IN_FAILED',
      ip,
      userAgent,
      metadata: { email: credentials.email },
    });

    throw error;
  }
}

Variables d'Environnement

Configuration Sécurisée

# .env.local - NE JAMAIS COMMITER

# Auth
NEXTAUTH_SECRET=votre-secret-genere-aleatoirement-min-32-chars
NEXTAUTH_URL=http://localhost:3000

# OAuth
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=xxx

# Database
DATABASE_URL=postgresql://user:password@host:5432/db?sslmode=require

# Autres
CSRF_SECRET=autre-secret-aleatoire
// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NEXTAUTH_SECRET: z.string().min(32),
  NEXTAUTH_URL: z.string().url(),
  DATABASE_URL: z.string().url(),
  GOOGLE_CLIENT_ID: z.string(),
  GOOGLE_CLIENT_SECRET: z.string(),
});

// Validation au démarrage
export const env = envSchema.parse(process.env);

Checklist de Sécurité

Avant de déployer, vérifiez :

Authentification

  • [ ] Mots de passe hashés avec bcrypt (coût >= 12)
  • [ ] Sessions JWT avec expiration
  • [ ] Protection contre l'énumération des utilisateurs
  • [ ] Rate limiting sur les endpoints d'auth

Autorisation

  • [ ] Middleware de protection des routes
  • [ ] Vérification des rôles côté serveur
  • [ ] Principe du moindre privilège

Protection des Données

  • [ ] Validation de toutes les entrées (Zod)
  • [ ] Sanitization du contenu utilisateur
  • [ ] Requêtes préparées (Prisma)
  • [ ] HTTPS obligatoire

Headers de Sécurité

  • [ ] Content-Security-Policy
  • [ ] X-Frame-Options
  • [ ] X-Content-Type-Options
  • [ ] Referrer-Policy

Monitoring

  • [ ] Logging des événements de sécurité
  • [ ] Alertes sur les échecs d'authentification
  • [ ] Audit trail des actions sensibles

Conclusion

La sécurité d'une application Next.js repose sur plusieurs couches de protection. Les points essentiels :

L'authentification est le premier rempart : Utilisez NextAuth.js avec des configurations strictes, hashage bcrypt, et considérez le 2FA pour les comptes sensibles.

Validez tout, ne faites confiance à rien : Chaque entrée utilisateur doit être validée et sanitizée. Utilisez Zod systématiquement.

Défense en profondeur : Middleware, headers de sécurité, validation côté serveur - chaque couche compte.

Auditez et monitorez : Les logs de sécurité permettent de détecter les attaques et de répondre rapidement aux incidents.

La sécurité n'est pas un état, c'est un processus continu. Restez informé des nouvelles vulnérabilités et mettez régulièrement à jour vos dépendances.


Besoin d'un audit de sécurité pour votre application ? Contactez Raicode pour une évaluation complète de votre infrastructure.

Partager :

Prêt à lancer votre projet ?

Transformez vos idées en réalité avec un développeur passionné par la performance et le SEO. Discutons de votre projet dès aujourd'hui.

Demander un devis
Voir mes réalisations
Réponse < 48h
15+ projets livrés
100% satisfaction client

Table des matières

Articles similaires

Les 10 Lignes de Code les Plus Dangereuses (Avec Exemples Réels)
development

Les 10 Lignes de Code les Plus Dangereuses (Avec Exemples Réels)

10 décembre 2025
10 min read
De 0 à Production en 4 Heures : Speed-Run d'un Site E-commerce
development

De 0 à Production en 4 Heures : Speed-Run d'un Site E-commerce

8 décembre 2025
15 min read
Intégrer l'IA dans Votre Application Web : Guide Pratique avec l'API OpenAI
development

Intégrer l'IA dans Votre Application Web : Guide Pratique avec l'API OpenAI

7 décembre 2025
15 min read
RAICODE

Développeur Full-Stack spécialisé en Next.js & React.
Je crée des applications web performantes et sur mesure.

SERVICES

  • Sites Vitrines
  • Applications SaaS
  • E-commerce
  • API & Backend

NAVIGATION

  • Processus
  • Projets
  • Blog
  • Tarifs
  • Contact

LÉGAL

  • Mentions légales
  • Confidentialité
  • CGU
  • CGV

© 2025 Raicode. Tous droits réservés.

Créé parRaicode.
↑ Retour en haut