# Plan d'Implémentation de l'Authentification Ce document détaille le plan d'implémentation du système d'authentification pour l'application de création de groupes, basé sur les spécifications du cahier des charges. ## 1. Vue d'Ensemble L'application utilisera OAuth 2.0 avec GitHub comme fournisseur d'identité, combiné avec une gestion de session basée sur JWT (JSON Web Tokens). Cette approche offre plusieurs avantages : - Délégation de l'authentification à un service tiers sécurisé (GitHub) - Pas besoin de gérer les mots de passe des utilisateurs - Récupération des informations de profil (nom, avatar) depuis l'API GitHub - Authentification stateless avec JWT pour une meilleure scalabilité ## 2. Flux d'Authentification ```mermaid sequenceDiagram participant User as Utilisateur participant Frontend as Frontend (Next.js) participant Backend as Backend (NestJS) participant GitHub as GitHub OAuth User->>Frontend: Clic sur "Se connecter avec GitHub" Frontend->>Backend: Redirection vers /auth/github Backend->>GitHub: Redirection vers GitHub OAuth GitHub->>User: Demande d'autorisation User->>GitHub: Accepte l'autorisation GitHub->>Backend: Redirection avec code d'autorisation Backend->>GitHub: Échange code contre token d'accès GitHub->>Backend: Retourne token d'accès Backend->>GitHub: Requête informations utilisateur GitHub->>Backend: Retourne informations utilisateur Backend->>Backend: Crée/Met à jour l'utilisateur en BDD Backend->>Backend: Génère JWT (access + refresh tokens) Backend->>Frontend: Redirection avec tokens JWT Frontend->>Frontend: Stocke les tokens Frontend->>User: Affiche interface authentifiée ``` ## 3. Structure des Modules ### 3.1 Module d'Authentification ```typescript // src/modules/auth/auth.module.ts import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { JwtModule } from '@nestjs/jwt'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthController } from './controllers/auth.controller'; import { AuthService } from './services/auth.service'; import { GithubStrategy } from './strategies/github.strategy'; import { JwtStrategy } from './strategies/jwt.strategy'; import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy'; import { UsersModule } from '../users/users.module'; @Module({ imports: [ PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ secret: configService.get('JWT_ACCESS_SECRET'), signOptions: { expiresIn: configService.get('JWT_ACCESS_EXPIRATION', '15m'), }, }), }), UsersModule, ], controllers: [AuthController], providers: [AuthService, GithubStrategy, JwtStrategy, JwtRefreshStrategy], exports: [AuthService], }) export class AuthModule {} ``` ### 3.2 Stratégies d'Authentification #### 3.2.1 Stratégie GitHub ```typescript // src/modules/auth/strategies/github.strategy.ts import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-github2'; import { ConfigService } from '@nestjs/config'; import { AuthService } from '../services/auth.service'; @Injectable() export class GithubStrategy extends PassportStrategy(Strategy, 'github') { constructor( private configService: ConfigService, private authService: AuthService, ) { super({ clientID: configService.get('GITHUB_CLIENT_ID'), clientSecret: configService.get('GITHUB_CLIENT_SECRET'), callbackURL: configService.get('GITHUB_CALLBACK_URL'), scope: ['read:user'], }); } async validate(accessToken: string, refreshToken: string, profile: any) { const { id, displayName, photos } = profile; const user = await this.authService.validateGithubUser({ githubId: id, name: displayName || `user_${id}`, avatar: photos?.[0]?.value, }); return user; } } ``` #### 3.2.2 Stratégie JWT ```typescript // src/modules/auth/strategies/jwt.strategy.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { UsersService } from '../../users/services/users.service'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { constructor( private configService: ConfigService, private usersService: UsersService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: configService.get('JWT_ACCESS_SECRET'), }); } async validate(payload: JwtPayload) { const { sub } = payload; const user = await this.usersService.findById(sub); if (!user) { throw new UnauthorizedException('User not found'); } return user; } } ``` #### 3.2.3 Stratégie JWT Refresh ```typescript // src/modules/auth/strategies/jwt-refresh.strategy.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { Request } from 'express'; import { UsersService } from '../../users/services/users.service'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; @Injectable() export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { constructor( private configService: ConfigService, private usersService: UsersService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: configService.get('JWT_REFRESH_SECRET'), passReqToCallback: true, }); } async validate(req: Request, payload: JwtPayload) { const refreshToken = req.headers.authorization?.replace('Bearer ', ''); if (!refreshToken) { throw new UnauthorizedException('Refresh token not found'); } const { sub } = payload; const user = await this.usersService.findById(sub); if (!user) { throw new UnauthorizedException('User not found'); } return { ...user, refreshToken }; } } ``` ### 3.3 Service d'Authentification ```typescript // src/modules/auth/services/auth.service.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { UsersService } from '../../users/services/users.service'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; import { TokensResponse } from '../interfaces/tokens-response.interface'; import { GithubUserDto } from '../dto/github-user.dto'; @Injectable() export class AuthService { constructor( private usersService: UsersService, private jwtService: JwtService, private configService: ConfigService, ) {} async validateGithubUser(githubUserDto: GithubUserDto) { const { githubId, name, avatar } = githubUserDto; // Recherche de l'utilisateur par githubId let user = await this.usersService.findByGithubId(githubId); // Si l'utilisateur n'existe pas, on le crée if (!user) { user = await this.usersService.create({ githubId, name, avatar, gdprTimestamp: new Date(), }); } else { // Mise à jour des informations de l'utilisateur user = await this.usersService.update(user.id, { name, avatar, }); } return user; } async login(user: any): Promise { const payload: JwtPayload = { sub: user.id }; const [accessToken, refreshToken] = await Promise.all([ this.generateAccessToken(payload), this.generateRefreshToken(payload), ]); return { accessToken, refreshToken, expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'), }; } async refreshTokens(userId: string, refreshToken: string): Promise { // Vérification du refresh token (à implémenter avec une table de tokens révoqués) const payload: JwtPayload = { sub: userId }; const [accessToken, newRefreshToken] = await Promise.all([ this.generateAccessToken(payload), this.generateRefreshToken(payload), ]); return { accessToken, refreshToken: newRefreshToken, expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'), }; } async logout(userId: string, refreshToken: string): Promise { // Ajouter le refresh token à la liste des tokens révoqués // À implémenter avec une table de tokens révoqués } private async generateAccessToken(payload: JwtPayload): Promise { return this.jwtService.signAsync(payload, { secret: this.configService.get('JWT_ACCESS_SECRET'), expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'), }); } private async generateRefreshToken(payload: JwtPayload): Promise { return this.jwtService.signAsync(payload, { secret: this.configService.get('JWT_REFRESH_SECRET'), expiresIn: this.configService.get('JWT_REFRESH_EXPIRATION', '7d'), }); } } ``` ### 3.4 Contrôleur d'Authentification ```typescript // src/modules/auth/controllers/auth.controller.ts import { Controller, Get, Post, UseGuards, Req, Res, Body, HttpCode } from '@nestjs/common'; import { Response } from 'express'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from '../services/auth.service'; import { JwtRefreshGuard } from '../guards/jwt-refresh.guard'; import { GetUser } from '../decorators/get-user.decorator'; import { RefreshTokenDto } from '../dto/refresh-token.dto'; import { Public } from '../decorators/public.decorator'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @Public() @Get('github') @UseGuards(AuthGuard('github')) githubAuth() { // Cette route redirige vers GitHub pour l'authentification } @Public() @Get('github/callback') @UseGuards(AuthGuard('github')) async githubAuthCallback(@Req() req, @Res() res: Response) { const { accessToken, refreshToken } = await this.authService.login(req.user); // Redirection vers le frontend avec les tokens const redirectUrl = `${this.configService.get('FRONTEND_URL')}/auth/callback?accessToken=${accessToken}&refreshToken=${refreshToken}`; return res.redirect(redirectUrl); } @Public() @Post('refresh') @UseGuards(JwtRefreshGuard) @HttpCode(200) async refreshTokens( @GetUser('id') userId: string, @GetUser('refreshToken') refreshToken: string, ) { return this.authService.refreshTokens(userId, refreshToken); } @Post('logout') @HttpCode(200) async logout( @GetUser('id') userId: string, @Body() refreshTokenDto: RefreshTokenDto, ) { await this.authService.logout(userId, refreshTokenDto.refreshToken); return { message: 'Logout successful' }; } @Get('profile') getProfile(@GetUser() user) { return user; } } ``` ### 3.5 Guards et Décorateurs #### 3.5.1 Guard JWT ```typescript // src/modules/auth/guards/jwt-auth.guard.ts import { Injectable, ExecutionContext } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Reflector } from '@nestjs/core'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor(private reflector: Reflector) { super(); } canActivate(context: ExecutionContext) { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) { return true; } return super.canActivate(context); } } ``` #### 3.5.2 Guard JWT Refresh ```typescript // src/modules/auth/guards/jwt-refresh.guard.ts import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {} ``` #### 3.5.3 Décorateur Public ```typescript // src/modules/auth/decorators/public.decorator.ts import { SetMetadata } from '@nestjs/common'; export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); ``` #### 3.5.4 Décorateur GetUser ```typescript // src/modules/auth/decorators/get-user.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const GetUser = createParamDecorator( (data: string | undefined, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user?.[data] : user; }, ); ``` ### 3.6 Interfaces et DTOs #### 3.6.1 Interface JwtPayload ```typescript // src/modules/auth/interfaces/jwt-payload.interface.ts export interface JwtPayload { sub: string; iat?: number; exp?: number; } ``` #### 3.6.2 Interface TokensResponse ```typescript // src/modules/auth/interfaces/tokens-response.interface.ts export interface TokensResponse { accessToken: string; refreshToken: string; expiresIn: string; } ``` #### 3.6.3 DTO GithubUser ```typescript // src/modules/auth/dto/github-user.dto.ts import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; export class GithubUserDto { @IsString() @IsNotEmpty() githubId: string; @IsString() @IsNotEmpty() name: string; @IsString() @IsOptional() avatar?: string; } ``` #### 3.6.4 DTO RefreshToken ```typescript // src/modules/auth/dto/refresh-token.dto.ts import { IsString, IsNotEmpty } from 'class-validator'; export class RefreshTokenDto { @IsString() @IsNotEmpty() refreshToken: string; } ``` ## 4. Configuration Globale de l'Authentification ### 4.1 Configuration du Module App ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { APP_GUARD } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './modules/auth/auth.module'; import { UsersModule } from './modules/users/users.module'; import { DatabaseModule } from './database/database.module'; import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'; import { validate } from './config/env.validation'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validate, }), DatabaseModule, AuthModule, UsersModule, ], controllers: [AppController], providers: [ AppService, { provide: APP_GUARD, useClass: JwtAuthGuard, }, ], }) export class AppModule {} ``` ### 4.2 Configuration CORS dans main.ts ```typescript // src/main.ts import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); // Configuration globale des pipes de validation app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, }), ); // Configuration CORS app.enableCors({ origin: configService.get('CORS_ORIGIN'), methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); // Préfixe global pour les routes API app.setGlobalPrefix(configService.get('API_PREFIX', 'api')); const port = configService.get('PORT', 3000); await app.listen(port); console.log(`Application is running on: http://localhost:${port}`); } bootstrap(); ``` ## 5. Sécurité et Bonnes Pratiques ### 5.1 Gestion des Tokens - **Access Token** : Durée de vie courte (15 minutes) pour limiter les risques en cas de vol - **Refresh Token** : Durée de vie plus longue (7 jours) pour permettre le rafraîchissement de l'access token - Stockage sécurisé des tokens côté client (localStorage pour l'access token, httpOnly cookie pour le refresh token dans une implémentation plus sécurisée) - Révocation des tokens en cas de déconnexion ou de suspicion de compromission ### 5.2 Protection contre les Attaques Courantes - **CSRF** : Utilisation de tokens anti-CSRF pour les opérations sensibles - **XSS** : Échappement des données utilisateur, utilisation de Content Security Policy - **Injection** : Validation des entrées avec class-validator, utilisation de paramètres préparés avec DrizzleORM - **Rate Limiting** : Limitation du nombre de requêtes d'authentification pour prévenir les attaques par force brute ### 5.3 Conformité RGPD - Enregistrement du timestamp d'acceptation RGPD lors de la création du compte - Possibilité d'exporter les données personnelles - Possibilité de supprimer le compte et toutes les données associées - Renouvellement du consentement tous les 13 mois ## 6. Intégration avec le Frontend ### 6.1 Flux d'Authentification côté Frontend ```typescript // Exemple de code pour le frontend (Next.js) // app/auth/github/page.tsx 'use client'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { API_URL } from '@/lib/constants'; export default function GitHubAuthPage() { const router = useRouter(); useEffect(() => { // Redirection vers l'endpoint d'authentification GitHub du backend window.location.href = `${API_URL}/auth/github`; }, []); return
Redirection vers GitHub pour authentification...
; } ``` ```typescript // app/auth/callback/page.tsx 'use client'; import { useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useAuth } from '@/hooks/useAuth'; export default function AuthCallbackPage() { const router = useRouter(); const searchParams = useSearchParams(); const { login } = useAuth(); useEffect(() => { const accessToken = searchParams.get('accessToken'); const refreshToken = searchParams.get('refreshToken'); if (accessToken && refreshToken) { // Stockage des tokens login(accessToken, refreshToken); // Redirection vers le dashboard router.push('/dashboard'); } else { // Erreur d'authentification router.push('/auth/error'); } }, [searchParams, login, router]); return
Finalisation de l'authentification...
; } ``` ### 6.2 Hook d'Authentification ```typescript // hooks/useAuth.tsx 'use client'; import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { jwtDecode } from 'jwt-decode'; import { API_URL } from '@/lib/constants'; interface AuthContextType { isAuthenticated: boolean; user: any | null; login: (accessToken: string, refreshToken: string) => void; logout: () => Promise; refreshAccessToken: () => Promise; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [user, setUser] = useState(null); const [accessToken, setAccessToken] = useState(null); const [refreshToken, setRefreshToken] = useState(null); useEffect(() => { // Récupération des tokens depuis le localStorage au chargement const storedAccessToken = localStorage.getItem('accessToken'); const storedRefreshToken = localStorage.getItem('refreshToken'); if (storedAccessToken && storedRefreshToken) { try { // Vérification de l'expiration du token const decoded = jwtDecode(storedAccessToken); const currentTime = Date.now() / 1000; if (decoded.exp && decoded.exp > currentTime) { setAccessToken(storedAccessToken); setRefreshToken(storedRefreshToken); setIsAuthenticated(true); fetchUserProfile(storedAccessToken); } else { // Token expiré, tentative de rafraîchissement refreshTokens(storedRefreshToken); } } catch (error) { // Token invalide, nettoyage clearTokens(); } } }, []); const fetchUserProfile = async (token: string) => { try { const response = await fetch(`${API_URL}/auth/profile`, { headers: { Authorization: `Bearer ${token}`, }, }); if (response.ok) { const userData = await response.json(); setUser(userData); } else { throw new Error('Failed to fetch user profile'); } } catch (error) { console.error('Error fetching user profile:', error); } }; const refreshTokens = async (token: string) => { try { const response = await fetch(`${API_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (response.ok) { const { accessToken: newAccessToken, refreshToken: newRefreshToken } = await response.json(); setAccessToken(newAccessToken); setRefreshToken(newRefreshToken); setIsAuthenticated(true); localStorage.setItem('accessToken', newAccessToken); localStorage.setItem('refreshToken', newRefreshToken); fetchUserProfile(newAccessToken); return newAccessToken; } else { throw new Error('Failed to refresh tokens'); } } catch (error) { console.error('Error refreshing tokens:', error); clearTokens(); return null; } }; const login = (newAccessToken: string, newRefreshToken: string) => { setAccessToken(newAccessToken); setRefreshToken(newRefreshToken); setIsAuthenticated(true); localStorage.setItem('accessToken', newAccessToken); localStorage.setItem('refreshToken', newRefreshToken); fetchUserProfile(newAccessToken); }; const logout = async () => { if (refreshToken) { try { await fetch(`${API_URL}/auth/logout`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ refreshToken }), }); } catch (error) { console.error('Error during logout:', error); } } clearTokens(); }; const clearTokens = () => { setAccessToken(null); setRefreshToken(null); setIsAuthenticated(false); setUser(null); localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); }; const refreshAccessToken = async () => { if (refreshToken) { return refreshTokens(refreshToken); } return null; }; return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; } ``` ### 6.3 Client API avec Gestion des Tokens ```typescript // lib/api-client.ts import { useAuth } from '@/hooks/useAuth'; import { API_URL } from './constants'; export function useApiClient() { const { isAuthenticated, refreshAccessToken } = useAuth(); const fetchWithAuth = async (endpoint: string, options: RequestInit = {}) => { if (!isAuthenticated) { throw new Error('User not authenticated'); } const accessToken = localStorage.getItem('accessToken'); const headers = { ...options.headers, 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }; try { const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers, }); // Si le token est expiré (401), on tente de le rafraîchir if (response.status === 401) { const newAccessToken = await refreshAccessToken(); if (newAccessToken) { // Nouvelle tentative avec le token rafraîchi return fetch(`${API_URL}${endpoint}`, { ...options, headers: { ...options.headers, 'Content-Type': 'application/json', Authorization: `Bearer ${newAccessToken}`, }, }); } else { throw new Error('Failed to refresh access token'); } } return response; } catch (error) { console.error('API request failed:', error); throw error; } }; return { fetchWithAuth }; } ``` ## 7. Tests ### 7.1 Tests Unitaires ```typescript // src/modules/auth/services/auth.service.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { UsersService } from