Récupérer l'utilisateur connecté avec un décorateur personnalisé

Récupérer l'utilisateur connecté avec un décorateur personnalisé

Dans une application NestJS sécurisée par JWT, l'utilisateur authentifié est généralement attaché à l'objet request par un guard (JwtAuthGuard dans notre cas). Pour y accéder dans un contrôleur, on utilise traditionnellement @Request() req puis req.user. Cette approche fonctionne, mais elle est verbeuse, peu typée et casse la lisibilité du code. Heureusement, NestJS offre une solution élégante : créer un décorateur de paramètre personnalisé @CurrentUser().

Dans ce tutoriel, nous allons construire pas à pas un décorateur réutilisable, le typer correctement avec TypeScript, et voir comment l'utiliser efficacement dans un projet réel.

Le problème : un accès verbeux et peu typé à l'utilisateur

Voici un extrait classique de contrôleur d'authentification :

@Get('me')
@ApiBearerAuth('JWT-auth')
async getProfile(@Request() req): Promise<ApiResponseDto<any>> {
  return ApiResponseDto.success(req.user, 'User retrieved successfully');
}

Plusieurs problèmes apparaissent :

  • Le paramètre req n'est pas typé : aucune autocomplétion sur req.user.
  • Le contrôleur dépend de l'objet HTTP brut, ce qui complique les tests unitaires.
  • Le code est répétitif : à chaque méthode, il faut écrire const user = req.user.
  • La sémantique est faible : la signature de la méthode ne dit pas clairement qu'elle dépend d'un utilisateur connecté.

L'objectif est d'arriver à une signature limpide :

@Get('me')
async getProfile(@CurrentUser() user: User) {
  return ApiResponseDto.success(user, 'User retrieved successfully');
}

Prérequis

  • Un projet NestJS 10+ fonctionnel.
  • Un système d'authentification JWT déjà en place avec un JwtAuthGuard qui injecte l'utilisateur dans request.user.
  • Une entité User (ici basée sur TypeORM) — dans notre exemple, l'utilisateur possède un id (UUID), un email, un role (enum UserRole), etc.

Comprendre createParamDecorator

NestJS expose une factory utilitaire, createParamDecorator, qui permet de créer un décorateur de paramètre. Sa signature est simple : on lui passe une fonction qui reçoit deux arguments — data (la valeur passée entre parenthèses lors de l'utilisation, ex. @CurrentUser('id')) et ctx, un ExecutionContext.

L'ExecutionContext est une abstraction puissante : elle permet à NestJS de gérer indifféremment HTTP, WebSocket ou GraphQL. Pour un contrôleur HTTP classique, on bascule vers le contexte HTTP avec ctx.switchToHttp() puis on récupère la requête avec .getRequest(). C'est exactement ce que fait JwtAuthGuard en interne.

Étape 1 : Créer le décorateur @CurrentUser()

Créons un fichier src/common/decorators/current-user.decorator.ts :

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '../../database/entities/user.entity';

/**
 * Décorateur de paramètre permettant d'extraire l'utilisateur authentifié
 * depuis la requête HTTP. L'utilisateur est injecté par le JwtAuthGuard.
 *
 * Usage :
 *   @CurrentUser() user: User           => retourne l'utilisateur complet
 *   @CurrentUser('id') userId: string   => retourne uniquement l'id (UUID)
 */
export const CurrentUser = createParamDecorator(
  (data: keyof User | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user: User | undefined = request.user;

    if (!user) {
      return undefined;
    }

    // Si une clé spécifique est demandée, on retourne uniquement cette propriété
    return data ? user[data] : user;
  },
);

Le décorateur est volontairement minimaliste : il ne fait que lire request.user, qui a été préalablement renseigné par JwtAuthGuard :

// Extrait de JwtAuthGuard
const user = await this.authService.validateToken(token);
request.user = user;
return true;

Point d'attention : ce décorateur ne fait aucune vérification d'authentification. La sécurité reste la responsabilité du guard. Si vous oubliez d'appliquer JwtAuthGuard, request.user sera undefined.

Étape 2 : Améliorer le typage TypeScript

La signature actuelle de data est keyof User | undefined, ce qui offre déjà l'autocomplétion sur les propriétés de User. Cependant, le type de retour reste implicitement any côté consommateur. Pour un typage encore plus strict, on peut utiliser des surcharges :

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '../../database/entities/user.entity';

const extractUser = (
  data: keyof User | undefined,
  ctx: ExecutionContext,
): User | User[keyof User] | undefined => {
  const request = ctx.switchToHttp().getRequest();
  const user: User | undefined = request.user;

  if (!user) {
    return undefined;
  }

  return data ? user[data] : user;
};

export const CurrentUser = createParamDecorator(extractUser);

En pratique, c'est l'annotation côté contrôleur (user: User ou userId: string) qui guide le typage final. Le décorateur ne peut pas inférer dynamiquement le type retourné en fonction de l'argument passé — c'est une limitation connue de TypeScript avec les décorateurs.

Étape 3 : Utilisation dans les contrôleurs

Reprenons notre AuthController. La version refactorée devient bien plus lisible :

import { Controller, Get, Post, Body } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { ApiResponseDto } from '../../common/dto/api-response.dto';
import { User } from '../../database/entities/user.entity';

@ApiTags('auth')
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Get('me')
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Get current user' })
  @ApiResponse({ status: 200, description: 'User information' })
  async getProfile(@CurrentUser() user: User): Promise<ApiResponseDto<User>> {
    return ApiResponseDto.success(user, 'User retrieved successfully');
  }

  @Post('refresh')
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Refresh JWT token' })
  async refresh(@CurrentUser() user: User): Promise<ApiResponseDto<any>> {
    const result = await this.authService.refreshToken(user);
    return ApiResponseDto.success(result, 'Token refreshed successfully');
  }
}

Variante : extraire un champ spécifique

Très souvent, on n'a besoin que de l'id de l'utilisateur — par exemple pour filtrer ses ressources. Le décorateur supporte ce cas nativement :

@Get('my-bookings')
@ApiBearerAuth('JWT-auth')
async getMyBookings(
  @CurrentUser('id') userId: string,
): Promise<ApiResponseDto<Booking[]>> {
  const bookings = await this.bookingsService.findByUser(userId);
  return ApiResponseDto.success(bookings, 'Bookings retrieved');
}

@Get('my-role')
@ApiBearerAuth('JWT-auth')
async getMyRole(@CurrentUser('role') role: UserRole) {
  return ApiResponseDto.success({ role }, 'Role retrieved');
}

Notez que userId est typé en string car l'entité User hérite de BaseEntity avec un identifiant UUID.

Étape 4 : Combiner avec des DTOs et la validation

Le décorateur @CurrentUser() brille particulièrement quand on le combine avec des DTOs validés. Imaginons un endpoint qui permet à un client de mettre à jour son profil :

import { IsOptional, IsString, MaxLength, IsPhoneNumber } from 'class-validator';

export class UpdateProfileDto {
  @IsOptional()
  @IsString()
  @MaxLength(100)
  firstName?: string;

  @IsOptional()
  @IsString()
  @MaxLength(100)
  lastName?: string;

  @IsOptional()
  @IsPhoneNumber('FR')
  phone?: string;
}

Et le contrôleur correspondant :

@Patch('me')
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Update current user profile' })
async updateProfile(
  @CurrentUser('id') userId: string,
  @Body() dto: UpdateProfileDto,
): Promise<ApiResponseDto<User>> {
  const updated = await this.usersService.update(userId, dto);
  return ApiResponseDto.success(updated, 'Profile updated successfully');
}

La séparation des responsabilités est nette : le DTO valide les données entrantes, le décorateur fournit l'identité de l'appelant, et le service applique la logique métier.

Étape 5 : Sécurité et bonnes pratiques

Voici quelques recommandations pour utiliser ce décorateur sereinement :

  • Toujours protéger les routes : @CurrentUser() ne remplace pas le guard. Si JwtAuthGuard est appliqué globalement (via APP_GUARD), assurez-vous que les routes publiques sont marquées avec @Public().
  • Ne jamais faire confiance au client : pour modifier ou supprimer une ressource, comparez toujours @CurrentUser('id') avec l'userId propriétaire de la ressource côté service.
  • Combiner avec @Roles() : pour les opérations privilégiées, ajoutez RolesGuard et le décorateur @Roles(UserRole.ADMIN).
  • Attention aux propriétés sensibles : dans notre entité, password est marqué { select: false }, donc il n'est pas chargé par défaut. Vérifiez tout de même que votre validateToken ne renvoie pas de données sensibles dans request.user.

Étape 6 : Tester le décorateur

Tester un createParamDecorator directement n'est pas trivial car il est compilé par Nest. La bonne pratique consiste à extraire la fonction d'extraction et à la tester unitairement :

// current-user.decorator.spec.ts
import { ExecutionContext } from '@nestjs/common';

// La fonction extraite est exportée séparément pour les tests
import { extractUser } from './current-user.decorator';

describe('CurrentUser decorator', () => {
  const buildContext = (user: any): ExecutionContext => ({
    switchToHttp: () => ({ getRequest: () => ({ user }) }),
  } as any);

  it('returns the full user when no key is provided', () => {
    const user = { id: 'uuid-1', email: 'a@b.com' };
    expect(extractUser(undefined, buildContext(user))).toEqual(user);
  });

  it('returns the requested property', () => {
    const user = { id: 'uuid-1', email: 'a@b.com' };
    expect(extractUser('email', buildContext(user))).toBe('a@b.com');
  });

  it('returns undefined when no user is attached', () => {
    expect(extractUser(undefined, buildContext(undefined))).toBeUndefined();
  });
});

Conclusion

Avec une vingtaine de lignes de code, nous avons créé un décorateur @CurrentUser() qui rend les contrôleurs NestJS beaucoup plus lisibles, mieux typés et plus testables. Ce pattern est totalement réutilisable d'un projet à l'autre : il suffit d'adapter le type User à votre entité.

Pour aller plus loin :

  • Créer un décorateur @CurrentUserId() dédié pour gagner encore en concision.
  • Adapter le décorateur au contexte GraphQL via GqlExecutionContext.create(ctx).getContext().req.
  • Combiner ce pattern avec d'autres décorateurs personnalisés (@ClientIp(), @UserAgent()) pour enrichir vos logs et audits.
  • Consulter la documentation officielle sur les décorateurs personnalisés.

En adoptant ces petits décorateurs utilitaires, vous éliminez progressivement le couplage entre vos contrôleurs et l'objet request brut, ce qui rend votre architecture plus alignée avec les principes SOLID — un bénéfice durable sur la maintenabilité de votre API.

Commentaires

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Les champs obligatoires sont indiqués avec *