> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# NestJS, un backend sécurisé avec Firebase
- URL: https://www.sfeir.dev/back/nestjs-un-backend-securise-avec-firebase/
- Published: 2024-06-21T08:18:04.000Z
- Updated: 2024-06-21T08:18:04.000Z
- Description: Cet article explore comment utiliser NestJS comme backend pour une Web App Angular hébergée par Firebase.
- Author: Stéphane Desplas
- Tags: Back, nestjs, Angular, firebase, GCP

Dans le développement d'applications web modernes, l'utilisation de frameworks efficaces pour le frontend et le backend est cruciale. [Angular](https://www.sfeir.dev/front/kesaco-angular/) et NestJS sont deux des frameworks les plus populaires pour le développement d'applications web. Firebase, bien qu'offrant des services puissants comme l'hébergement et l'authentification, ne fournit pas un backend traditionnel. Pour exploiter une base de données MongoDB et enrichir notre application avec des API tierces tout en maintenant des appels API sécurisés, j'ai voulu tester NestJS comme backend. Cet article explore comment utiliser NestJS comme backend pour une Web App Angular hébergée par Firebase.

## Use case: WhatsApp

Pour contextualiser ce projet, nous allons nous inspirer des fonctionnalités de base de WhatsApp en implémentant des services backend permettant d'envoyer un message, changer son statut, modifier et supprimer le message.

#### Pourquoi Angular, NestJS et Firebase ?

1. [**Angular**](https://www.sfeir.dev/front/pourquoi-utiliser-angular/) : Un framework frontend développé par Google, idéal pour construire des applications web dynamiques avec une architecture modulaire.
2. **NestJS** : Un framework backend progressif construit sur Node.js, qui utilise TypeScript et suit les principes de l'architecture modulaire, ce qui en fait un complément naturel à Angular.
3. [**Firebase** ](https://www.sfeir.dev/cloud/une-web-app-en-prod-avec-firebase/): Une plateforme de développement d'applications web et mobiles de Google, qui offre des services d'hébergement, de base de données en temps réel, d'authentification, et bien plus.

J'ai consacré [un article complet sur l'utilisation de Firebase](https://www.sfeir.dev/cloud/une-web-app-en-prod-avec-firebase), n'hésitez pas à le consulter pour mieux comprendre tous les avantages de cet outil !

## Architecture

Notre Web App va exploiter Firebase pour : 

- le module d'authentification pour gérer les sessions utilisateurs
- un compte de service pour valider les token de session
- le hosting

La Web App sera construite avec Angular en utilisant : 

- AngularFire pour se connecter simplement à Firebase
- un interceptor pour ajouter le token (généré par Firebase) dans le header des call de notre API (backend NestJS)

Le backend (API) sera sécurisé car : 

- pour chaque requête, NestJS appellera Firebase via un compte de service pour valider le token
- pour un token valide, une réponse avec un code HTTP 200 sera renvoyée à la Web App, sinon HTTP 401

## NestJS

NestJS est un framework pour développer des applications backend en utilisant Node.js. Il est écrit en TypeScript, ce qui permet d'avoir un code plus structuré et moins sujet aux erreurs. NestJS offre une architecture modulaire et une grande extensibilité, facilitant ainsi la création d'applications web, d'API et de microservices.

Un bon parallèle pour comprendre NestJS est de le comparer à Spring Boot, un framework populaire pour Java. Tout comme Spring Boot, NestJS est conçu pour rendre le développement d'applications côté serveur plus simple et plus organisé. Les deux frameworks partagent des concepts clés comme l'injection de dépendances, les modules et les contrôleurs, ce qui permet aux développeurs de structurer leur code de manière claire et maintenable.

En résumé, si vous connaissez Spring Boot pour Java, imaginez un outil similaire pour Node.js : c'est ce que propose NestJS, avec des fonctionnalités modernes et une forte intégration avec TypeScript.

## Installation

`npm install -g @nestjs/cli  
nest new my-whatsapp-webapp  
cd my-whatsapp-webapp`

Vous avez vu le code généré ? On dirait Angular n'est ce pas ? 🤩  
On va maintenant créer le model, le service et le controller dans `/src`.

`mkdir models services`

Ajoutez le model *Message* dans models : 

```typescript
export interface Message {
  id?: string;
  content: string;
  sender: string;
  recipient: string;
  status: string;
  timestamp?: Date;
}
```

Avant de créer le service, voyons le controller : `messages.controller.ts`

```typescript
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import { MessagesService } from './messages.service';
import { Message } from './interfaces/message.interface';

@Controller('messages')
export class MessagesController {
  constructor(private readonly messagesService: MessagesService) {}

  @Get()
  async findAll(): Promise<Message[]> {
    return this.messagesService.findAll();
  }

  @Post()
  async create(@Body() message: Message): Promise<Message> {
    return this.messagesService.create(message);
  }

  @Put(':id')
  async update(@Param('id') id: string, @Body() message: Message): Promise<Message> {
    return this.messagesService.update(id, message);
  }

  @Delete(':id')
  async delete(@Param('id') id: string): Promise<Message> {
    return this.messagesService.delete(id);
  }
}

```

Les annotations @Get(), @Post(), @Put() et @Delete() permettent de créer notre API Rest très simplement. Voyons maintenant la couche service.

Nous allons utiliser une base de donnée MongoDB pour persister nos messages. Voici la dépendances à installer et le service : `npm install @nestjs/mongoose mongoose`

```typescript
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Message } from './interfaces/message.interface';

@Injectable()
export class MessagesService {
  constructor(@InjectModel('Message') private messageModel: Model<Message>) {}

  async findAll(): Promise<Message[]> {
    return this.messageModel.find().exec();
  }

  async create(message: Message): Promise<Message> {
    const newMessage = new this.messageModel(message);
    return newMessage.save();
  }

  async update(id: string, message: Message): Promise<Message> {
    return this.messageModel.findByIdAndUpdate(id, message, { new: true }).exec();
  }

  async delete(id: string): Promise<Message> {
    return this.messageModel.findByIdAndDelete(id).exec();
  }
}

```

Nous avons besoin de décrire notre model pour MongoDB, pour cela il faut ajouter le schéma correspondant à *Message.*

Voici le code de `message.schema.ts` à créer dans `/src/schemas`

```typescript
import { Schema } from 'mongoose';

export const MessageSchema = new Schema({
  content: { type: String, required: true },
  sender: { type: String, required: true },
  recipient: { type: String, required: true },
  status: { type: String, required: true },
  timestamp: { type: Date, default: Date.now },
});

```

Et enfin sa déclaration dans *app.module.ts*

```typescript
imports: [
  MongooseModule.forFeature(
    [{ name: 'Message', schema: MessageSchema }]
  ),
],
```

Créez votre base MongoDB et récupérer l'url de connexion de type : `mongodb+srv://stephanedesplas:......`

La configuration se passe dans les imports de *app.module.ts*

```typescript
MongooseModule.forRoot('mongodb+srv://stephanedesplas:....')
```

Voilà notre API REST est prêt à accepter les requêtes.

Démarrez le serveur NestJS avec `npm start` et depuis Postman exécutez une requête POST `localhost:3000/messages` avec le corps suivant : 

```json
{
    "content": "test",
    "sender": "steph",
    "recipient": "sfeir",
    "status": "sended"
}

```

Une nouvelle requête avec la même URL en GET confirme que le document a bien été persisté : 

```json
[
    {
        "_id": "666178a67eed888902ba41b9",
        "content": "test",
        "sender": "steph",
        "recipient": "sfeir",
        "status": "sended",
        "timestamp": "2024-06-06T08:51:50.945Z",
        "__v": 0
    }
]
```

## Sécurisation de l'API

Créez un projet Firebase et installer le Firebase CLI en vous aidant de [mon article dédié](https://www.sfeir.dev/cloud/une-web-app-en-prod-avec-firebase/).

Dans la console, dans les paramètres du projet ouvrez l'onglet *Comptes de service* (sert à authentifier plusieurs fonctionnalités Firebase, telles que Database, Storage et Auth, de manière automatisée via le SDK Admin). Générez une clef privée.

![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/2024/06/Screenshot-2024-06-10-at-14.36.42.png)

Interface comptes de service

Nous allons créer le service `firebase.service.ts` qui va nous permettre de vérifier la validité d'un token.

```typescript
import * as admin from 'firebase-admin';
import { Injectable } from '@nestjs/common';

@Injectable()
export class FirebaseService {
  constructor() {
    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: process.env.FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
      }),
    });
  }

  async verifyToken(token: string): Promise<admin.auth.DecodedIdToken> {
    return admin.auth().verifyIdToken(token);
  }
}

```

Pour tester le projet, vous pouvez remplacer temporairement les variables d'environnement par leurs valeurs dans le fichier JSON généré précédemment.

Maintenant, nous allons créer un guard qui sera appelé à chaque requête reçue :

```typescript
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common';
import { FirebaseService } from '../services/firebase.service';

@Injectable()
export class FirebaseAuthGuard implements CanActivate {
  constructor(private firebaseService: FirebaseService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);

    if (!token) {
      throw new UnauthorizedException('No token provided');
    }

    try {
      const decodedToken = await this.firebaseService.verifyToken(token);
      request.user = decodedToken;
      console.log('token valid: ', request.user);
      return true;
    } catch (err) {
      throw new UnauthorizedException('Invalid token');
    }
  }

  private extractTokenFromHeader(request): string | null {
    const authorization = request.headers.authorization;
    if (!authorization) {
      return null;
    }
    const [bearer, token] = authorization.split(' ');
    return bearer === 'Bearer' ? token : null;
  }
}

```

Enfin nous décorons nos services sensibles avec `@UseGuards(FirebaseAuthGuard)`

```typescript
@UseGuards(FirebaseAuthGuard)
@Get()
async findAll(): Promise<Message[]> {
  return this.messagesService.findAll();
}
...
```

Notre API n'acceptera que les requêtes ayant un token valide ! 🥳

## Le Front end avec Angular

Nous allons créer le service qui permet d'utiliser le module d'authentification de Firebase. Vous devez au préalable configurer votre projet avec le module Firebase comme dans l'article plus haut.

```typescript
import { inject, Injectable } from '@angular/core';
import {
  Auth,
  createUserWithEmailAndPassword,
  GoogleAuthProvider,
  signInWithEmailAndPassword,
  signInWithPopup,
  signOut,
  user,
} from '@angular/fire/auth';
import { UserCredential } from '@firebase/auth';

@Injectable({
  providedIn: 'root',
})
export class AuthenticationService {
  private auth: Auth = inject(Auth);
  private provider = new GoogleAuthProvider();
  user$ = user(this.auth);

  async login(): Promise<any> {
    return await signInWithPopup(this.auth, this.provider);
  }

  async logout(): Promise<any> {
    return signOut(this.auth);
  }

  async signUp(email: string, password: string): Promise<UserCredential> {
    return createUserWithEmailAndPassword(this.auth, email, password);
  }

  async signIn(email: string, password: string): Promise<UserCredential> {
    return signInWithEmailAndPassword(this.auth, email, password);
  }

  getCurrentUser() {
    return this.auth.currentUser;
  }

  isUserSignedIn() {
    return !!this.auth.currentUser;
  }
}
```

Nous créons ensuite un interceptor qui va injecter le token généré dans les headers des requêtes vers l'API :

```typescript
import {
  HttpEvent,
  HttpHandlerFn,
  HttpInterceptorFn,
  HttpRequest,
} from '@angular/common/http';
import { inject } from '@angular/core';
import { from, lastValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { Auth } from '@angular/fire/auth';

const addBearerToken = async (
  req: HttpRequest<any>,
  next: HttpHandlerFn,
): Promise<HttpEvent<any>> => {
  const angularFireAuth = inject(Auth);
  const firebaseUser = angularFireAuth.currentUser;
  const token = await firebaseUser?.getIdToken();
  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }
  return lastValueFrom(next(req));
};
export const bearerTokenInterceptor: HttpInterceptorFn = (req, next) => {
  // on limite les requêtes à 'http://localhost:3000'
  if (req.url.startsWith(environment.backendUrl)) {
    return from(addBearerToken(req, next));
  } else {
    return next(req);
  }
};

```

Dernière étape, nous déclarons cet interceptor dans `app.module.ts`

```
providers: [provideHttpClient(withInterceptors([bearerTokenInterceptor]))]
```

## Conclusion

En suivant les étapes de cet article, vous pouvez configurer une application NestJS sécurisée avec Firebase, assurant une meilleure protection des données et une expérience utilisateur optimisée. Cette méthode est adaptable à vos besoins spécifiques, rendant vos solutions plus sûres et évolutives.

Pour une solution efficace et simple, l'association de Firebase, Angular, et NestJS est idéale. Firebase offre un système d'authentification robuste, NestJS permet de développer des API sécurisées, et Angular complète parfaitement le frontend. Cette stack harmonieuse répond aux exigences les plus élevées en matière de sécurité et de performance.

Pour plus de détails ou si vous envisagez de tester cette stack, n'hésitez pas à [me contacter sur LinkedIn](https://www.linkedin.com/in/st%C3%A9phane-desplas-2849bb59?ref=sfeir.dev)!