👤 Implémenter un système d'authentification via LinkedIn avec React & NestJS

messagegit

Grégory CHEVALLIER

Posted on October 17, 2023

👤 Implémenter un système d'authentification via LinkedIn avec React & NestJS

L'implémentation d'un système d'authentification via LinkedIn peut présenter de nombreux avantages pour votre plateforme.

Dans un premier temps, il permet de vous prémunir contre les bots & les logiciels automatisés 🤖. En effet, la création d'un compte LinkedIn requiert le renseignement d'un numéro de téléphone. On peut dès lors considérer qu'un utilisateur authentifié via LinkedIn auprès de votre plateforme est une personne réelle 🙍‍♂️.

Mais l'authentification via LinkedIn permet surtout l'obtention de données ayant trait à l'identité réelle de l'utilisateur se connectant à votre plateforme. Il s'agit donc d'une solution pertinente pour obtenir le profil professionnel d'un internaute.

Je vous propose, dans cet article, de découvrir comment implémenter une solution d'authentification via LinkedIn en créant : une application React ainsi qu'une API NestJS.

Source Github 👇
https://github.com/MessageGit/oauth-sign-in-with-linkedin.


⚠️ Pré-requis

Pour implémenter un système d'authentification via LinkedIn, vous devez tout d'abord :

  • Être détenteur d'un compte utilisateur LinkedIn valide (Si vous n'avez pas de compte LinkedIn, créez-en un en cliquant ici).
  • Être propriétaire d'une page professionnelle LinkedIn (à l'effigie de votre plateforme) (Si vous n'avez pas de page professionnelle, créez-en une en cliquant ici).
  • Créer une application LinkedIn (nécessite de satisfaire les deux pré-requis précédents). Pour cela, rendez-vous sur la page de création d'une nouvelle application puis complétez le formulaire en renseignant les informations suivantes : nom de l'application, URL de votre page professionnelle, upload d'un logo, acceptation des CGU.

Image description

[...] Votre nouvelle application, une fois créée, devrait apparaître dans le tabeau de bord « développeurs » de votre compte LinkedIn, cliquez-ici pour y accéder.

Il ne vous reste plus qu'à cliquer sur votre nouvelle application pour accéder à son panel de gestion 🎉
(Voir exemple ci-dessous)

Image description


⚙️ Configuration préalable via LinkedIn Developers

Le fonctionnement de la connexion OAuth 2 via LinkedIn ne diffère pas grandement des autres services qui l'intègrent (comme par exemple la connexion via Google/Facebook).

Un système de « scopes » est présent et permet à l'utilisateur de réguler l'accord des permissions léguées à l'application, point positif en faveur de l'aspect sécuritaire de la plateforme.

ℹ️ Si la présence de « scopes » semble être standard, rappelons tout de même que certain(e)s réseaux sociaux/applications n'intègrent pas ce système précautionneux voué à limiter l'exploitation des ressources par une application tierce. C'est le cas de la messagerie sécurisée Telegram notamment.

Par défaut, votre application (fraîchement créée dans le chapitre précédent) n'intègre pas la connexion via LinkedIn, il faudra l'activer manuellement en vous rendant dans la rubrique « Products » de votre application (toujours dans le panel de gestion de votre application LinkedIn developers).

Cliquez ensuite sur le bouton « Request access » lié au produit nommé « Sign In with LinkedIn ».

Une fois le produit activé, deux nouveaux scopes feront leur apparition dans la section « OAuth 2.0 scopes » de la rubrique « Auth » : r_emailaddress ainsi que r_liteprofile.

C'est dans cette même rubrique que vont se manifester de nombreuses informations essentielles à l'implémentation de la solution d'authentification via LinkedIn.

La section « Application credentials » va répertorier le Client ID & le Client Secret, deux informations primordiales permettant d'identifier votre application lorsque vos futurs utilisateurs accéderont au portail d'authentification via LinkedIn.

La section « OAuth 2.0 settings » quant à elle va permettre d'autoriser des URLs de redirection pour votre application. Une fois l'authentification de vos utilisateurs validée, LinkedIn les redirigera vers l'URL demandée (sous réserve que cette dernière soit autorisée dans cette même section).

Image description

Une fois la configuration de l'application via le panel LinkedIn developers terminée, il est temps de passer à l'implémentation technique du système d'authentification !


📸 Développement de la web application React

Avant de débuter l'initialisation de notre application React, il me semble judicieux de résumer les opérations à entreprendre via l'application front pour procéder à l'authentification d'un utilisateur LinkedIn.

👤 Aspect procédural de l'authentification client

1ère étape de l'authentification 👇

Rediriger le client vers le portail d'authentification LinkedIn en y apportant des informations telles que : l'URI de redirection souhaité (dans l'éventualité où l'authentification de notre client est un succès), les scopes requis par notre application, l'identifiant même de notre application, etc..

2ème étape de l'authentification 👇

Partons du principe que notre client soit parvenu à se connecter à LinkedIn : ce dernier est dès lors redirigé à l'URI que nous avons fournit à LinkedIn lorsque nous l'avons renvoyé vers le portail d'authentification. Sa redirection vers notre URI personnalisée est accompagnée d'un query params « code », il s'agit là d'un « authorization code » que nous enverrons à notre API pour obtenir un droit d'exploitation des ressources de notre client (sous réserve que l'exploitation soit faite par notre API exclusivement).

[...] Nous verrons dans le chapitre suivant comment échanger le code d'autorisation, résultant de la connexion de notre client à LinkedIn puis comment obtenir l'autorisation d'exploitation des ressources LinkedIn de notre client.

🌐 Initialisation de la web application React

Maintenant que nous avons étudié l'aspect procédural de l'authentification côté client, tâchons de l'implémenter via une application React.

Commençons par initialiser une nouvelle application React en saisissant la commande suivante dans un terminal : npx create-react-app linkedin-oauth-app.

Nous idéaliserons une architecture/arborescence d'application très minimaliste et assez peu élaborée telle que la suivante :

    > linkedin-oauth-app
        > node_modules
        > public
        > src
            > api
            > assets
            App.tsx
            index.css
            index.js
        .env
        .env.example
        .gitignore
        package.json
        README.md
        tsconfig.json
Enter fullscreen mode Exit fullscreen mode

🗒️ Déclaration des variables d'environnement

Déclarons sans plus attendre les variables d'environnement indispensables au fonctionnement de notre système d'authentification, et plus globalement, au fonctionnement de notre application :

    # MAIN
    REACT_APP_API_URL=

    # LINKEDIN
    REACT_APP_LINKEDIN_CLIENT_ID=
    REACT_APP_LINKEDIN_SCOPES=
    REACT_APP_LINKEDIN_REDIRECT_URI=
Enter fullscreen mode Exit fullscreen mode

[...] Notre fichier .env (registre des variables d'environnement) sera identique au fichier .env.example (ci-dessus), à la seule différence que celui-ci intégrera des valeurs.

🤔 Pourquoi un fichier .env et .env.example ?

ℹ️ Le principe de cette mesure sécuritaire est simple. Pour celles & ceux qui n'en ont pas connaissance : le fichier .env, référencé dans notre fichier .gitignore, peut inclure une infinité d'informations sensibles selon la complexité de l'application, il n'a donc pas vocation à intégrer notre répertoire Github.
Notre fichier .env.example est, quant à lui, le modèle des variables d'environnement. Il sera respectivement publié sur notre répertoire Github à titre d'exemple et n'intégrera donc aucune valeur.

Vous pouvez d'ores et déjà incrémenter la clé REACT_APP_LINKEDIN_SCOPES par la valeur suivante : r_liteprofile,r_emailaddress. Cette dernière correspond à la solution « Sign-In with LinkedIn ».

Vous retrouverez la valeur de la clé REACT_APP_LINKEDIN_CLIENT_ID et REACT_APP_LINKEDIN_REDIRECT_URI dans le panel LinkedIn Developers de votre application, rubrique « Auth ».

La clé REACT_APP_API_URL correspondra quant à elle à l'URL de votre API, mais nous ne l'avons pas encore créée, nous y reviendrons donc ultérieurement 😜

Dans notre fichier App.tsx, nous importerons les variables d'environnement correspondantes à LinkedIn sous forme d'object Javascript :

import React from 'react';

const App = () => {
    /* Load LinkedIn credentials from dotenv */
    const LINKEDIN_ENV = {
        CLIENT_ID: process.env['REACT_APP_LINKEDIN_CLIENT_ID'],
        SCOPES: process.env['REACT_APP_LINKEDIN_SCOPES'],
        REDIRECT_URI: process.env['REACT_APP_LINKEDIN_REDIRECT_URI'],
    }

    // (...)
}
Enter fullscreen mode Exit fullscreen mode

📤 Implémentation technique et redirection client

A titre d'exemple, un simple bouton « Se connecter via LinkedIn » permettant la redirection du client au portail d'authentification LinkedIn fera l'affaire. Inutile de configurer plusieurs pages ou d'aborder un développement plus complexe, l'enjeu de cet exemple étant de saisir le fonctionnement de l'authentification.

Choisissez un jolie logo LinkedIn, il intégrera notre bouton personnalisé « Se connecter via LinkedIn » et nous permettra de rediriger notre client à l'adresse suivante :

https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${LINKEDIN_ENV.CLIENT_ID}&scope=${LINKEDIN_ENV.SCOPES}&redirect_uri=${LINKEDIN_ENV.REDIRECT_URI}

Décomposons cette adresse pour l'analyser & la comprendre :

https://www.linkedin.com/oauth/v2/authorization correspond à l'URL du portail d'authentification.

?response_type=code précise que nous attendons un code d'authorization.

&client_id=${LINKEDIN_ENV.CLIENT_ID} permet d'identifier notre application auprès des serveurs LinkedIn.

&scope=${LINKEDIN_ENV.SCOPES} permet de spécifier les scopes requis par notre application.

&redirect_uri=${LINKEDIN_ENV.REDIRECT_URI} permet de préciser l'URI à laquelle LinkedIn doit rediriger le client connecté.

import React from 'react';

import LinkedInIcon from './assets/linkedin_icon.png';

const App = () => {

  /* Load LinkedIn credentials from dotenv */
  const LINKEDIN_ENV = {
    CLIENT_ID: process.env['REACT_APP_LINKEDIN_CLIENT_ID'],
    SCOPES: process.env['REACT_APP_LINKEDIN_SCOPES'],
    REDIRECT_URI: process.env['REACT_APP_LINKEDIN_REDIRECT_URI'],
  }

  /* Handle the client redirection to LinkedIn authentication portal */
  const onSignInLinkedIn = () => {
    window.location.replace(`https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${LINKEDIN_ENV.CLIENT_ID}&scope=${LINKEDIN_ENV.SCOPES}&redirect_uri=${LINKEDIN_ENV.REDIRECT_URI}`);
  }

  return (
    <div className="App">

        <button className="LinkedIn_Button" onClick={onSignInLinkedIn}>
          <img src={LinkedInIcon} alt="Logo LinkedIn" className="LinkedIn_Icon" />
          <span>Se connecter via LinkedIn</span>
        </button>

    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

A ce stade, le bouton « Se connecter via LinkedIn » nous redirige vers le portail d'authentification LinkedIn :

Image description

Si une erreur survient, vérifiez que les variables d'environnement saisies correspondent bien aux credentials fournis pour votre application, que le produit « Sign-In with LinkedIn » soit bien activé, que les scopes spécifiés soient valides & que l'URI de redirection transmise via l'URI du portail d'authentification soit autorisée via le panel de gestion de votre application depuis LinkedIn developers.

ℹ️ N'oubliez pas de re-démarrer votre application React une fois les variables d'environnement modifiées. Celles-ci ne se synchronisent pas automatiquement lors de leur modification.

Si la configuration de votre application est correcte & que le formulaire d'authentification s'affiche sans problèmes, essayez alors de vous connecter :

Image description

L'application requiert dès lors, comme prévu, les scopes préalablement configurés tels que :

  • Les informations basiques du profil utilisateur (= Utiliser votre nom et photo)
  • L'adresse e-mail de l'utilisateur (= Utiliser l'adresse e-mail principale associée à votre compte LinkedIn)

[...] Il ne vous reste plus qu'à autoriser l'exploitation de ces scopes par l'application pour être redirigé à l'URI transmise au préalable, accompagné d'un « authorization code » qui se présente sous forme de query params, tel que :

?code=AQVumAQMn0w5BWznTRD0nnMRDA_fBYNzBBHe3yxmOxMQvm9nW0yw8b7QrUNffvuEkSYlYOXU26FGL7vvm7W0-oe8pqyDFXj5w6JxW3A0bhCLPIXQsYMZfP_9QwRrJDPDdjZIYhPILu0xfGEWaM44Isy6FxYTtzMLf1HxkXdqGUVoJjR_TGPEpC-c3Dcj-oqJue6I7NgsurIZltYFuQw

ℹ️ Notez qu'en procédant à cette authentification, vous léguez des permissions d'exploitation des ressources liées à votre profil LinkedIn, à une application tierce. Pour révoquer ces permissions, vous pouvez vous rendre dans les préférences de votre profil LinkedIn puis dans : Confidentialité des données > Autres applications > Services autorisés > Nom de l'application > Supprimer.

📥 Récupération du code d'autorisation et transmission à l'API

Actuellement, nous avons un bouton permettant de rediriger le client vers le portail d'authentification LinkedIn. Si la connexion d'un utilisateur est valide, il est redirigée vers notre application React, avec un code d'autorisation.

L'enjeu est maintenant de récupérer ce code d'autorisation pour le transmettre à notre API.

Toujours dans notre fichier App.tsx, tâchons sans plus tarder de le récupérer :

    /* Check if a Linkedin authorization code is provided */
    const authorizationCode = new URLSearchParams(window.location.search).get('code');
Enter fullscreen mode Exit fullscreen mode

La constante authorizationCode intègre désormais le code d'autorisation retourné par LinkedIn.

A défaut, cette dernière sera respectivement undefined. Il ne nous reste plus qu'à traiter ce code dès lorsque notre App.tsx est initialisé, dans l'hypothèse que ce dernier ne soit pas undefined. Nous utiliserons le hook React useEffect pour se faire :

import React, {useEffect} from 'react';

import LinkedInIcon from './assets/linkedin_icon.png';

const App = () => {

    /* Load LinkedIn credentials from dotenv */
    const LINKEDIN_ENV = {
        CLIENT_ID: process.env['REACT_APP_LINKEDIN_CLIENT_ID'],
        SCOPES: process.env['REACT_APP_LINKEDIN_SCOPES'],
        REDIRECT_URI: process.env['REACT_APP_LINKEDIN_REDIRECT_URI'],
    }

    /* Handle the client redirection to LinkedIn authentication portal */
    const onSignInLinkedIn = () => {
        window.location.replace(`https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${LINKEDIN_ENV.CLIENT_ID}&scope=${LINKEDIN_ENV.SCOPES}&redirect_uri=${LINKEDIN_ENV.REDIRECT_URI}`);
    }

    /* Check if a Linkedin authorization code is provided */
    const authorizationCode = new URLSearchParams(window.location.search).get('code');

    useEffect(() => {
        if (authorizationCode) {
          // TODO: Send authorization code to server here
        }
    }, [authorizationCode]);

    return (
        <div className="App">

            <button className="LinkedIn_Button" onClick={onSignInLinkedIn}>
              <img src={LinkedInIcon} alt="Logo LinkedIn" className="LinkedIn_Icon" />
              <span>Se connecter via LinkedIn</span>
            </button>

        </div>
    );

}

export default App;
Enter fullscreen mode Exit fullscreen mode

Dans notre hook useEffect, le code d'autorisation peut enfin être exploité (sous réserve qu'il soit définit).

Installons à présent la librairie axios, librairie très réputée permettant d'émettre des requêtes. Nous pourrions également utiliser n'importe quelle autre librairie de ce type, voir utiliser simplement l'API fetch intégrée nativement dans Javascript et permettant de d'exploiter des requêtes AJAX.

Une fois la librairie installée, nous créerons une instance axios qui se basera sur la variable d'environnement REACT_APP_API_URL préalablement déclarée :

    import axios from 'axios';

    const baseURL = process.env['REACT_APP_API_URL'];
    export default axios.create({ baseURL, withCredentials: true });
Enter fullscreen mode Exit fullscreen mode

Une fois que notre API sera créée et exposée, nous n'aurons qu'à incrémenter notre variable d'environnement REACT_APP_API_URL par l'URL de notre API et utiliser l'instance déclarée ci-dessus pour échanger avec cette dernière.

Revenons dans notre fichier App.tsx, importons notre instance axios puis requêtons le futur end-point de notre API auquel nous enverrons le code d'autorisation reçu :

import React, {useEffect} from 'react';
import API from './api/api'; // import of our axios instance

import LinkedInIcon from './assets/linkedin_icon.png';

const App = () => {

    /* Load LinkedIn credentials from dotenv */
    const LINKEDIN_ENV = {
        CLIENT_ID: process.env['REACT_APP_LINKEDIN_CLIENT_ID'],
        SCOPES: process.env['REACT_APP_LINKEDIN_SCOPES'],
        REDIRECT_URI: process.env['REACT_APP_LINKEDIN_REDIRECT_URI'],
    }

    /* Handle the client redirection to LinkedIn authentication portal */
    const onSignInLinkedIn = () => {
        window.location.replace(`https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${LINKEDIN_ENV.CLIENT_ID}&scope=${LINKEDIN_ENV.SCOPES}&redirect_uri=${LINKEDIN_ENV.REDIRECT_URI}`);
    }

    /* Check if a Linkedin authorization code is provided */
    const authorizationCode = new URLSearchParams(window.location.search).get('code');

    useEffect(() => {
        if (authorizationCode) {
            API.post('linkedin/auth/login', { authorization_code: authorizationCode })
                .then((res) => console.log('Request received', res))
                .catch((err) => console.log('An error occured', err));
        }
    }, [authorizationCode]);

    return (
        <div className="App">

            <button className="LinkedIn_Button" onClick={onSignInLinkedIn}>
              <img src={LinkedInIcon} alt="Logo LinkedIn" className="LinkedIn_Icon" />
              <span>Se connecter via LinkedIn</span>
            </button>

        </div>
    );

}

export default App;
Enter fullscreen mode Exit fullscreen mode

Dans l'implémentation de notre requête ci-dessus, nous spéculons à la fois sur :

  • l'end-point cible de notre API (nous avons choisi linkedin/auth/login)
  • la dénomination de la clé passée dans le body de notre requête (nous avons choisi authorization_code)

📌 Récapitulons :

Si l'on part du principe que l'URL de notre futur API est http://localhost:8080/ et que la variable d'environnement REACT_APP_API_URL est respectivement incrémentée par cette valeur/url, notre application React émettra ici une requête POST à l'adresse http://localhost:8080/linkedin/auth/login avec une clé authorization_code en body incluant la valeur du code d'autorisation retourné par LinkedIn pour notre utilisateur.


🏗️ Développement de l'API avec NestJS

Maintenant que notre application web est fonctionnelle et capable de transmettre le code d'autorisation client à l'API, conçevoir l'API il va falloir pour exploiter les données de l'utilisateur LinkedIn en toute sécurité.

Nous utiliserons NestJS, un framework Typescript basé sur Node.js mais vous pouvez évidement choisir un framework ou une technologie plus à votre aise selon vos habitudes.

Ci-dessous, nous verrons comment entreprendre l'implémentation via NestJS.

🧰 Initialisation du serveur & configuration préalable

Une fois NestJS installé, initialisez un nouveau projet via la commande : nest new linkedin-oauth-api.

Encore une fois, nous privilégierons une arborescence de projet simpliste/minimaliste et une architecture en microservices, NestJS étant idéal pour ce type d'architecture.

    > linkedin-oauth-api
        > dist
        > node_modules
        > src
            > config
            > microservices
            app.module.ts
            main.ts
        .env
        .env.example
        .eslintrc.js
        .gitignore
        .prettierrc
        nest-cli.json
        package.json
        tsconfig.build.json
        tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Au même titre que notre application web, configurons les fichiers relatifs aux variables d'environnement .env et .env.example :

    PORT=8080
    NODE_ENV=development

    LINKEDIN_API_CLIENT_ID=
    LINKEDIN_API_CLIENT_SECRET=
    LINKEDIN_API_REDIRECT_URI=
Enter fullscreen mode Exit fullscreen mode

Il ne tient plus qu'à vous de le compléter selon les credentials fournis pour votre application LinekdIn.

ℹ️ Rappel : retrouvez les credentials liées à votre application via le panel LinkedIn developers, au sein de la rubrique « Auth ».

Pour lire les variables d'environnement avec NestJS, nous aurons besoin d'installer une librairie conçue par les soins des développeurs du framework, à savoir : @nestjs/config, utilisez donc npm i --save @nestjs/config si ce n'est pas déjà fait.

Initialisez ensuite le fichier root > src > config > config.ts en y ajoutant le code suivant :

interface LinkedInConfig {
    clientId: string;
    clientSecret: string;
    redirectUri: string;
}

interface ConfigProps {
    port: number;
    linkedIn: LinkedInConfig;
}

export const config = (): ConfigProps => ({
    port: parseInt(process.env.PORT, 10) || 8080,
    linkedIn: {
        clientId: process.env.LINKEDIN_API_CLIENT_ID,
        clientSecret: process.env.LINKEDIN_API_CLIENT_SECRET,
        redirectUri: process.env.LINKEDIN_API_REDIRECT_URI,
    }
});
Enter fullscreen mode Exit fullscreen mode

Même si nous avons ajouté la clé NODE_ENV par convention, aux fichiers relatifs aux variables d'environnement, nous n'en ferons pas grand chose dans ce tutoriel.

Une fois le fichier de config configuré et les types ajoutés, il faudra dorénavant l'importer dans le fichier app.module.ts comme suit :

import { Module } from '@nestjs/common';

/* NestJS Config */
import { ConfigModule } from '@nestjs/config'; // import of our @nestjs/config lib.
import { config } from './config/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [config]
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

[...] Le plus haut fichier de votre projet main.ts devra intégrer le code suivant :

import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config'; // import of our @nestjs/config lib.

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors({
    origin: true,
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
    credentials: true,
  });

  const configService = app.get(ConfigService);
  const port = configService.get<number>('port'); // get port from dotenv file

  await app.listen(port, '0.0.0.0');
  console.log(`\n\n\x1b[34m🚀 Server is now available at the \x1b[37m${await app.getUrl()}\x1b[34m address!\x1b[0m`);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

🎉 Notre API peut maintenant être démarrée en mode développement via la commande : npm run start:dev.

⚙️ Initialisation de notre nouveau microservice

Maintenant que notre API est exposée via http://localhost:8080/, il nous faut créer la route permettant l'échange du code d'autorisation reçu par notre web-application contre un jeton d'accès permettant l'exploitation des ressources utilisateurs. Créons alors le microservice linkedin.

    > microservices
        > linkedin
            > api
                linkedin.api.ts // next chapter
            > controllers
                linkedin.controller.ts
            > objects
                linkedin-user-profile.object.ts // next chapter
                linkedin-user-token.object.ts // next chapter
            > services
                linkedin.service.ts
            linkedin.module.ts
Enter fullscreen mode Exit fullscreen mode

1️⃣ Déclaration du service linkedin

Le fichier src > microservices > linkedin > services > linkedin.service.ts nous permettra d'implémenter l'intégralité des méthodes liées à LinkedIn.

import { Injectable } from '@nestjs/common';

@Injectable()
export class LinkedInService {
    constructor() {}

    async login(authorizationCode: string): Promise<any> {
        console.log('> Authorization code received :', authorizationCode);
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

2️⃣ Déclaration du controlleur linkedin

Le fichier src > microservices > linkedin > controllers > linkedin.controller.ts permettra de définir la route d'accès, gérer la réception des données fournis dans le corps de la requête et s'assurera de transmettre les informations aux méthodes de notre service préalablement déclaré.

import { Body, Controller, Post } from '@nestjs/common';

import { LinkedInService } from 'src/microservices/linkedin/services/linkedin.service';

@Controller('linkedin')
export class LinkedInController {
  constructor(private readonly linkedInService: LinkedInService) {}

  @Post('auth/login')
  async login(@Body('authorization_code') authorizationCode: string): Promise<any> {
      return await this.linkedInService.login(authorizationCode);
  }

}
Enter fullscreen mode Exit fullscreen mode

3️⃣ Déclaration du module linkedin

Le fichier src > microservices > linkedin > linkedin.module.ts fera office de porte d'accès à notre controlleur puis à notre service, il assurera l'import global des fichiers préalablement déclarés et permettra d'y greffer des modules / services complémentaires, il est une base rattaché au corps de notre serveur.

import { Module } from '@nestjs/common';

import { LinkedInController } from 'src/microservices/linkedin/controllers/linkedin.controller';
import { LinkedInService } from 'src/microservices/linkedin/services/linkedin.service';

@Module({
  imports: [],
  controllers: [LinkedInController],
  providers: [LinkedInService],
})
export class LinkedInModule {}
Enter fullscreen mode Exit fullscreen mode

4️⃣ Import de notre module auprès du module global

Notre fichier app.module.ts est l'un des fichiers des plus haut-niveau dans l'architecture de notre application, il est en tout cas le module parent à notre serveur. C'est auprès de ce dernier qu'il nous faudra s'assurer de l'import de notre nouveau module linkedin.

import { Module } from '@nestjs/common';

/* NestJS Config */
import { ConfigModule } from '@nestjs/config';
import { config } from './config/config';

/* Imported modules */
import { LinkedInModule } from './microservices/linkedin/linkedin.module'; // import of LinkedIn module

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [config]
    }),
    LinkedInModule, // import of LinkedIn module
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

A ce stade, la connexion LinkedIn via notre web-application React doit être capable de provoquer l'impression du code d'autorisation via notre serveur 🎉

🔐 Obtention d'un jeton d'accès depuis un code d'autorisation

Maintenant que nous reçevons le code d'autorisation au sein de la méthode login(...) de notre service LinkedIn, tâchons de l'échanger à LinkedIn contre un jeton d'accès.

Pour commencer, nous créerons le SDK LinkedIn. Celui-ci nous permettra de faire appel à des méthodes qui, quant à elles, se chargeront de formuler les requêtes adéquates auprès des serveurs de LinkedIn. Il s'agit ni plus ni moins que de déclarer une instance axios (comme nous l'avons fait avec React) et de déclarer des méthodes invoquant cette instance, en ciblant des routes spécifiques.

import { ConfigService } from "@nestjs/config";
import axios from "axios";

const linkedInAuthentication = () => {
    const axiosInstance = axios.create({
        baseURL: 'https://www.linkedin.com/oauth/v2',
        withCredentials: true,
    });
    return axiosInstance;
}
export const loginLinkedIn = async (code: string) => /* Get accessToken from an authorization code */
    await linkedInAuthentication().get('/accessToken', {
        params: {
            grant_type: 'authorization_code',
            client_id: new ConfigService().get('LINKEDIN_API_CLIENT_ID'),
            client_secret: new ConfigService().get('LINKEDIN_API_CLIENT_SECRET'),
            code,
            redirect_uri: new ConfigService().get('LINKEDIN_API_REDIRECT_URI'),
        },
    });
export const checkSessionValidity = async (accessToken: string) => {/* Introspect and verify an accessToken */
    const body = {
        client_id: new ConfigService().get('LINKEDIN_API_CLIENT_ID'),
        client_secret: new ConfigService().get('LINKEDIN_API_CLIENT_SECRET'),
        token: accessToken,
    };
    return await linkedInAuthentication().post('/introspectToken', new URLSearchParams(body), {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
}
Enter fullscreen mode Exit fullscreen mode

La méthode checkSessionValidity ci-dessus est d'une importance toute particulière.

En effet, nombreu.x.ses sont les développeu.r.ses à vérifier l'état de validité d'un jeton d'accès en essayant d'obtenir des ressources utilisateurs. Le problème de ce procédé est le suivant : l'obtention de ressources liées à un utilisateur augmente le quota d'exploitation de votre application pour une route spécifique.

Si à priori, en phase de test et/ou lors d'une utilisation de votre application à faible traffic, aucun problème ne survient, l'application montrera vite des signes de faiblesse et rendra des fonctionnalités obsolètes lors de son évolution.

Image description

La route cible de la méthode checkSessionValidity n'a, quant à elle, aucun impact sur le quota d'utilisation des services par votre application. Elle peut être sollicitée par un guard, decorator mais encore un middleware pour attester ou non de la validité d'un jeton d'accès utilisateur.

ℹ️ Les plateformes comme LinkedIn, Facebook, Google etc.. se prémunissent de la surexploitation de leurs serveurs en instaurant ces limitations à quota réinitialisable.

[...] Après avoir installé la librairie class-validator en utilisant npm i --save class-validator, il nous faudra créer l'objet LinkedInUserTokenObject, cet object incluera le jeton d'accès utilisateur ainsi que sa date d'expiration.

import { IsNumber, IsString } from "class-validator";

export class LinkedInUserTokenObject {
    @IsString()
    accessToken: string;

    @IsNumber()
    expireIn: number;
}
Enter fullscreen mode Exit fullscreen mode

Revenons maintenant à la méthode login(...) de notre service linkedin.service.ts :

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { loginLinkedIn } from 'src/microservices/linkedin/api/linkedin.api';

import { LinkedInUserTokenObject } from '../objects/linkedin-user-token.object';

@Injectable()
export class LinkedInService {
    constructor() {}

    async login(authorizationCode: string): Promise<LinkedInUserTokenObject> {
        try {
            const response = await loginLinkedIn(authorizationCode);
            const { access_token: accessToken, expires_in: expireIn } = response.data;
            return { accessToken, expireIn }
        } catch (err) {
            throw new UnauthorizedException('Unable to login as LinkedIn user from the provided authorization code!');
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Puis à notre controlleur linkedin.controller.ts :

import { Body, Controller, Post } from '@nestjs/common';

import { LinkedInService } from 'src/microservices/linkedin/services/linkedin.service';
import { LinkedInUserTokenObject } from '../objects/linkedin-user-token.object';

@Controller('linkedin')
export class LinkedInController {
    constructor(private readonly linkedInService: LinkedInService) {}

    @Post('auth/login')
    async login(@Body('authorization_code') authorizationCode: string): Promise<LinkedInUserTokenObject> {
        const token: LinkedInUserTokenObject = 
            await this.linkedInService.login(authorizationCode); // Get permissions from LinkedIn server(s)
        return token;
    }

}
Enter fullscreen mode Exit fullscreen mode

Si nous inspectons à présent l'onglet "Réseau/Network" de notre navigateur, une fois la redirection depuis le portail d'authentification LinkedIn effectuée, on peut remarquer que la requête émise avec le code d'autorisation reçoit bel & bien une réponse incluant les propriétés de l'utilisateur : accessToken et expireIn.

Evidemment, nous retournons ces informations uniquement pour analyser le résultat et savoir notre code fonctionnel.

Nous allons maintenant voir comment obtenir les informations d'un utilisateur depuis ce précieux accessToken que nous obtenons.

📥 Exploitation des ressources liées à un utilisateur LinkedIn

Pour exploiter les ressources auxquelles notre application est autorisée à accéder, une mise à jour de notre SDK LinkedIn s'impose, nous ajouterons dans notre fichier src > microservices > linkedin > api > linkedin.api.ts, x2 nouvelles routes permettant de récupérer :

  • l'adresse e-mail de l'utilisateur
  • le nom/prénom & image de profil de l'utilisateur
import { ConfigService } from "@nestjs/config";
import axios from "axios";

/* (1) LinkedIn API | global endpoint */
const linkedInAPI = () => {
    const axiosInstance = axios.create({
        baseURL: 'https://api.linkedin.com/v2',
        withCredentials: true,
    });
    return axiosInstance;
}
/* [LinkedIn] Get email address */
export const getLinkedInEmailAddress = async (accessToken: string) => 
    linkedInAPI().get(`/emailAddress?q=members&projection=(elements*(handle~))`, {headers: { Authorization: `Bearer ${accessToken}` }});
/* [LinkedIn] Get (first/last)name and profile picture */
export const getLinkedInProfile = async (accessToken: string) => 
    linkedInAPI().get(`/me?projection=(id,firstName,lastName,emailAddress,profilePicture(displayImage~:playableStreams))`, {headers: { Authorization: `Bearer ${accessToken}` }});


/* (2) Authentication */
const linkedInAuthentication = () => {
    const axiosInstance = axios.create({
        baseURL: 'https://www.linkedin.com/oauth/v2',
        withCredentials: true,
    });
    return axiosInstance;
}
export const loginLinkedIn = async (code: string) => /* Get accessToken from an authorization code */
    await linkedInAuthentication().get('/accessToken', {
        params: {
            grant_type: 'authorization_code',
            client_id: new ConfigService().get('LINKEDIN_API_CLIENT_ID'),
            client_secret: new ConfigService().get('LINKEDIN_API_CLIENT_SECRET'),
            code,
            redirect_uri: new ConfigService().get('LINKEDIN_API_REDIRECT_URI'),
        },
    });
export const checkSessionValidity = async (accessToken: string) => {/* Introspect and verify an accessToken */
    const body = {
        client_id: new ConfigService().get('LINKEDIN_API_CLIENT_ID'),
        client_secret: new ConfigService().get('LINKEDIN_API_CLIENT_SECRET'),
        token: accessToken,
    };
    return await linkedInAuthentication().post('/introspectToken', new URLSearchParams(body), {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    });
}
Enter fullscreen mode Exit fullscreen mode

Le type de l'object LinkedInUserProfileObject ayant pour vocation de répertorier les informations de notre utilisateur doit être créé :

import { IsNumber, IsString } from "class-validator";

export class LinkedInUserProfileObject {
    @IsString()
    firstName: string;

    @IsString()
    lastName: string;

    @IsString()
    email: string;

    @IsString()
    profileImageUrl: string;
}
Enter fullscreen mode Exit fullscreen mode

Une nouvelle méthode permettant de récupérer le profil utilisateur depuis l'accessToken obtenu doit ensuite être implémentée dans notre fichier linkedin.service.ts :

import { Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common';
import { getLinkedInEmailAddress, getLinkedInProfile, loginLinkedIn } from 'src/microservices/linkedin/api/linkedin.api';

import { LinkedInUserTokenObject } from '../objects/linkedin-user-token.object';
import { LinkedInUserProfileObject } from '../objects/linkedin-user-profile.object';

@Injectable()
export class LinkedInService {
    constructor() {}

    async login(authorizationCode: string): Promise<LinkedInUserTokenObject> {
        try {
            const response = await loginLinkedIn(authorizationCode);
            const { access_token: accessToken, expires_in: expireIn } = response.data;
            return { accessToken, expireIn }
        } catch (err) {
            throw new UnauthorizedException('Unable to login as LinkedIn user from the provided authorization code!');
        }
    }

    async getProfile(accessToken: string): Promise<LinkedInUserProfileObject> {
        /* (1) Get email of LinkedIn client */
        const req1 = await getLinkedInEmailAddress(accessToken);
        const email = req1?.data?.elements[0]['handle~'].emailAddress;

        /* (2) Get (first/last)name and profile image of LinkedIn client */
        const req2 = await getLinkedInProfile(accessToken);
        const firstName = req2?.data?.firstName?.localized?.fr_FR;
        const lastName = req2?.data?.lastName?.localized?.fr_FR;
        const profileImageUrl = req2?.data?.profilePicture['displayImage~']?.elements[0]?.identifiers[0]?.identifier;

        if (!firstName || !lastName || !profileImageUrl || !email) throw new InternalServerErrorException('An error occured while parsing the retrieved profile from LinkedIn!');
        return {
            firstName,
            lastName,
            email,
            profileImageUrl,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Il ne nous reste plus qu'à modifier notre fichier linkedin.controller.ts pour retourner les informations de l'utilisateur plutôt que de retourner le jeton d'accès client :

import { Body, Controller, Post } from '@nestjs/common';

import { LinkedInService } from 'src/microservices/linkedin/services/linkedin.service';
import { LinkedInUserTokenObject } from '../objects/linkedin-user-token.object';
import { LinkedInUserProfileObject } from '../objects/linkedin-user-profile.object';

@Controller('linkedin')
export class LinkedInController {
  constructor(private readonly linkedInService: LinkedInService) {}

  @Post('auth/login')
  async login(@Body('authorization_code') authorizationCode: string): Promise<any> {
    const token: LinkedInUserTokenObject = 
        await this.linkedInService.login(authorizationCode); // Get permissions from LinkedIn server(s)
    const profile: LinkedInUserProfileObject = 
        await this.linkedInService.getProfile(token.accessToken); // Get profile from LinkedIn server(s)
    return profile;
  }

}
Enter fullscreen mode Exit fullscreen mode

Félicitations 🎉

Les informations de l'utilisateur ayant procédé à l'authentification LinkedIn sont dorénavant renvoyées au client.

Il ne tient plus qu'à vous d'utiliser ces informations selon vos ambitions, vous pourriez par exemple :

- Sauvegarder les informations liées à cet utilisateur en base de données dans le cadre d'une connexion/inscription via LinkedIn.
- Garder en cache ou retourner les informations de l'utilisateur et les sauvegarder en cache front-end (via cookies http-only ou local storage, etc..)
- etc..


🎁 Bonus

-> (Source) Retrouvez l'intégralité du code qui constitue ce tutoriel via ce répertoire Github.

💖 💪 🙅 🚩
messagegit
Grégory CHEVALLIER

Posted on October 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related