brief-20/docs/implementation/AUTH_IMPLEMENTATION_PLAN.md
Avnyr f6f0888bd7 docs: add comprehensive project documentation files
Added detailed documentation files, including project overview, current status, specifications, implementation guide, and README structure. Organized content to improve navigation and streamline project understanding.
2025-05-15 17:08:53 +02:00

17 KiB

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

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

// 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<string>('JWT_ACCESS_SECRET'),
        signOptions: {
          expiresIn: configService.get<string>('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

// 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<string>('GITHUB_CLIENT_ID'),
      clientSecret: configService.get<string>('GITHUB_CLIENT_SECRET'),
      callbackURL: configService.get<string>('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

// 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<string>('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

// 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<string>('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

// 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<TokensResponse> {
    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<string>('JWT_ACCESS_EXPIRATION', '15m'),
    };
  }

  async refreshTokens(userId: string, refreshToken: string): Promise<TokensResponse> {
    // 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<string>('JWT_ACCESS_EXPIRATION', '15m'),
    };
  }

  async logout(userId: string, refreshToken: string): Promise<void> {
    // 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<string> {
    return this.jwtService.signAsync(payload, {
      secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
      expiresIn: this.configService.get<string>('JWT_ACCESS_EXPIRATION', '15m'),
    });
  }

  private async generateRefreshToken(payload: JwtPayload): Promise<string> {
    return this.jwtService.signAsync(payload, {
      secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
      expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRATION', '7d'),
    });
  }
}

3.4 Contrôleur d'Authentification

// 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<string>('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

// 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<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;
    }

    return super.canActivate(context);
  }
}

3.5.2 Guard JWT Refresh

// 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

// 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

// 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

// 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

// 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 <div>Redirection vers GitHub pour authentification...</div>;
}
// 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 <div>Finalisation de l'authentification...</div>;
}

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.