# 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; }, ); ``` ## 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 {} ``` ## 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...
; } ``` ## 7. Conclusion Ce plan d'implémentation fournit une base solide pour mettre en place un système d'authentification sécurisé et conforme aux bonnes pratiques. L'utilisation d'OAuth 2.0 avec GitHub comme fournisseur d'identité simplifie le processus d'authentification pour les utilisateurs tout en offrant un niveau de sécurité élevé. La gestion des sessions avec JWT permet une architecture stateless qui facilite la scalabilité de l'application, tandis que le mécanisme de refresh token offre une expérience utilisateur fluide sans compromettre la sécurité. Les mesures de sécurité et de conformité RGPD intégrées dans ce plan garantissent que l'application respecte les normes actuelles en matière de protection des données et de sécurité des utilisateurs.