Background color
A black and white photo of a bench.
Engineering
7
minutes
2022-02-23

Sécuriser votre application avec le SSO Google et JWT

Comment sécuriser votre application avec le Google SSO et sécuriser votre cookie JWT contre les attaques CRSF ? Explication avec Amel.

Amel
Développeuse web
Dans cet article

Dans cet article, vous trouverez deux mises en situations différentes.

La première est la volonté de mettre en place un système d’authentification des utilisateurs via Google.

Et la deuxième concerne la sécurisation du JWT (JSON Web Token) contre les attaques CRSF.

Première situation : Authentification avec Google SSO

Admettons que notre application soit hébergée sur Google Cloud Platform. Nous voulons que seuls nos employés puissent accéder à notre application et tous nos employés utilisent un compte Google pour accéder à leurs outils. Nous devons rendre possible leur authentification via le Google SSO.

Comment ça marche ?

Albert est notre nouvel employé pour lequel nous venons de créer un compte Google. Il veut se connecter à notre application interne SpaceY.

  1. Albert se connecte à https://spacey.hubvisoryapp.com et la page authentification apparait.
  2. Lorsqu’il clique sur “Se connecter avec Google”, une requête HTTP Get est envoyée au backend de SpaceY : https://api.spacey.hubvisory.com/.
  3. Ensuite le back-end nous redirige vers le page d’authentification de Google. Albert se connecte et est redirigé vers le dashboard de SpaceY. En effet, une fois l’authentification effectuée, Google SSO fournit un jeton d’accès au backend via une redirection. Ce dernier va créer un JWT à partir de ce jeton, le mettre dans les cookies de la réponse HTTP et engendrer une redirection vers le dashboard du front-end.
Schéma représentant le workflow architectural de l’utilisateur qui veut se connecter à l’application SpaceY Source

Schéma représentant le workflow architectural de l’utilisateur qui veut se connecter à l’application SpaceY - Source (image) : https://fr.depositphotos.com/90095458/stock-illustration-albert-einstein-cartoon.html

Quelles sont les étapes ?

1) Configurer GCP

API credentials

Une fois que Google a authentifié l’utilisateur, une requête HTTP contenant le jeton d’accès doit être délivrée au back-end. Pour ce faire, nous devons ajouter notre URL aux URLs de redirection autorisées à recevoir ces informations :

Ecran de consentement OAuth
  • Créer un client ID
  • Sélectionner ce client ID
Sélection du client ID
  • Ajouter l’URL de redirection du backend
Redirection du backend

2) Créer une route dans le backend pour nous rediriger vers la page d’authentification de Google

Dans cet exemple on utilise un serveur Nest express.

Il faut ajouter ces packages au package.json :

"passport": "^0.4.1","passport-google-oauth20": "^2.0.0",

Le package [Passport](https://docs.nestjs.com/security/authentication) implémente un mécanisme pour authentifier les utilisateurs de notre application. En utilisant [AuthGuard](https://docs.nestjs.com/guards), nous pouvons créer et utiliser plusieurs stratégies d’authentifications.

Le package [passport-google-oauth20](https://www.npmjs.com/package/passport-google-oauth20) contient une stratégie “google” guard qui peut déclencher une redirection vers la page d’authentification de Google. Pour ce faire, nous allons créer une route au niveau du contrôleur de l’application dont le seul but est d’impliquer cette redirection :

@Controller()export class AppController {@UseGuards(AuthGuard('google'))  @Get()  getAuthenticated() {}}

Quand le client envoie une requête à la route getAuthenticated de l’API, il va être redirigé vers la page d’authentification de Google. Une fois que l’utilisateur est connecté à Google, le SSO de Google va engendrer une redirection vers une route spécifique de notre application (voir la configuration de GCP).

3) Créer une route qui va recevoir les données de l’utilisateur une fois qu’il s’est connecté au Google SSO.

Comme spécifié dans la section de la configuration de GCP, nous avons autorisé notre application à recevoir les données de l’utilisateur connecté via une redirection sur celle-ci. Nous devons donc créer cette dite route.

La requête contient des données d’authentification de Google, comme le jeton d’accès. En utilisant ce jeton d’accès, on peut générer un JWT. Un JWT est un jeton signé qui assure que l’utilisateur est connecté. La réponse HTTP va engendrer une redirection vers le front-end et contenir le JWT dans les cookies.

Il existe un package qui implémente l’encodage et le décodage du JWT. Dans le fichier package.json, nous devons ajouter :

"@nestjs/jwt": "^8.0.0",

Ce package contient un JwtService, ce dernier mettant à disposition deux méthodes :

  • sign: encode le payload
  • verify: vérifie et décode le payload

Un payload est un object d’objects qui semblent nécessaires pour constituer le JWT.

Le JWT_SECRET est une chaîne de caractères utilisée pour encoder le payload.

export type JwtPayload = { [key: string]: any };@Injectable()export class JwtAuthService {  constructor(    private jwtService: JwtService,    @Inject(JWT_SECRET) private jwtSecret: JwtSecretI  ) {}  login(user: { [key: string]: any }): { accessToken: string } {    const payload: JwtPayload = user;    return {      accessToken: this.jwtService.sign(payload, {        secret: this.jwtSecret.secret,      }),    };  }  verify(token: string): { [key: string]: any } {    return this.jwtService.verify(token, this.jwtSecret);  }}

Retournons à la création de la route /redirect.

La requête contient certaines données de l’utilisateur provenant de Google. Par exemple, on connait l’adresse email, le nom de l’utilisateur et son jeton d’accès. On considère comme le payload l’object suivant : {email: "fake.email@gmail.com", googleAccessToken: "token"}.

Alors nous pouvons appeler jwtAuthService.login(payload) pour l’encoder en tant que JWT.

Pour finir, nous ajoutons ce JWT dans les cookies de la réponse et on redirige le client.

@Get('redirect')  async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {   const user: User | undefined = req['user'];    if (!user || !user.email) {      throw new HttpException(        { status: HttpStatus.UNAUTHORIZED, error: 'Wrong authentication' },        HttpStatus.UNAUTHORIZED      );    }    let payload = await this.userService.payload(user);    payload.googleAccessToken = user.accessToken;    const { accessToken } = this.jwtAuthService.login(payload);    res.cookie('jwt', accessToken, {      httpOnly: true,      secure: true,      sameSite: 'none',    });    let uri: string | undefined = this.redirectService.getRedirectUri();    if (uri !== undefined) {      res.redirect(uri);    } else res.send('You are authenticated to Google SSO');    }

Deuxième situation : sécuriser votre application de l’attaque CRSF

1) L’attaque

Angelina est connectée à son compte en banque, donc un cookie est disponible dans son navigateur web. Brad veut lui voler un million de dollars. Pour ce faire, il lui envoie un lien sur Whatsapp qui redirige vers un meme très marrant.

Angelina clique sur le lien et pourtant rien ne s’affiche. Mais maintenant elle peut trouver - 1.000.000 de dollars sur son compte en banque.

Mais qu’est-ce qu’il s’est passé ?

Malheureusement elle vient d’être victime d’une attaque CSRF (ou XSRF). En fait, au lieu de lui envoyer une image, Brad lui a envoyé un lien vers un script qui envoie une requête à l’API de sa banque pour transférer ce million de dollar sur son propre compte bancaire

2) La solution

  • Utiliser les requêtes Http POST avec un body pour les actions qui changent le système.
  • Donner l’accès à l’API seulement aux clients autorisés (CORS).
  • Ajouter une confirmation de l’utilisateur via l’interface utilisateur pour les actions importantes (comme la suppression, l’ajout)
  • Vérifier que la page précédente, le référent, correspond bien à une page du client.
  • Ajouter une vérification dans la requête HTTP à l’aide du jeton XSRF.

Développons ce dernier point.

3) Sécurisez vos requêtes : le jeton XSRF.

Pour s’assurer que les requêtes de l’API viennent d’un client autorisé :

Au niveau du back-end :

L’utilisateur demande à s’authentifier. Quand l’utilisateur est authentifié par un SSO ou autre méthode d’authentification, on obtient un jeton d’accès.

Nous allons générer un autre jeton, le jeton XSRF. J’aime utiliser un UUID pour ça.

Nous allons ensuite l’ajouter au JWT, qui contenait déjà le jeton d’accès. Ce dernier sera alors une chaine de caractères encodée qui contient les deux jetons XSRF et d’accès. En Javascript :

const jwt : string = encode({accessToken: "accessToken", xsrfToken: "xsrfToken"})

On place ce JWT dans les cookies de la réponse HTTP en tant que cookie HTTPOnly, pour être sûr qu’aucun code JS ne pourra y avoir accès.

response.cookie('jwt', jwt, {      httpOnly: true,      secure: true,      sameSite: 'none',    });

On ajoute le jeton XSRF dans l’en-tête.

response.cookie('xsrfToken', xsrfToken,{      httpOnly: false,      secure: true,      sameSite: 'none');

Au niveau du front-end :

  • Nous devons nous assurer que le jeton XSRF est intercepté par le front-end. Avec Angular, on utilise un module appelé HttpClientXsrfModule.
  • HttpClientXsrfModule.withOptions({cookieName: 'xsrfToken',headerName: 'x_xsrf_token',}),
  • Dorénavant, chaque requête sortante contiendra un en-tête x_xsrf_token dont la valeur est celle du jeton XSRF envoyée par le back-end.
  • Note : avec ce module Angular, si on requête le back-end avec un chemin absolu, il faudra mettre soi-même le jeton xsrf dans l’en-tête, comme ce qui suit :
export class AppHttpInterceptor implements HttpInterceptor {constructor(private tokenExtractor: HttpXsrfTokenExtractor) {}
intercept(
 request: HttpRequest<any>,
 next: HttpHandler
): Observable<HttpEvent<any>> {
 const headerName: string = 'x_xsrf_token';
 const xsrfToken: string | null = this.tokenExtractor.getToken();

 if (xsrfToken && !request.headers.has(headerName)) {
   request = request.clone({
     headers: request.headers.set(headerName, xsrfToken),
   });
 }

 request = request.clone({
   withCredentials: true,
 });

 return next.handle(request);
}
}

Retour au backend :

  • Il faut maintenant comparer le jeton XSRF contenu dans l’en-tête de la requête et celui qui se trouve dans le JWT. S’ils sont identiques, super, on peut accepter la requête. Sinon, il faut lancer une exception HTTP.

Pour conclure

Nous avons sécurisé notre application par l’authentification mais aussi en protégeant nos cookies. Il existe bien d’autres formes de sécurisation telle que la gestion des autorisations d’accès aux données ou bien l’utilisation d’une passerelle pour protéger notre API.

Parlons produit

Échangeons sur votre produit

Nous croyons en un nouveau modèle de consulting où l’excellence commence par l’écoute, le partage et une vraie vision

background color