👤 Implémenter un système d'authentification via LinkedIn avec React & NestJS
Grégory CHEVALLIER
Posted on October 17, 2023
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.
[...] 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)
⚙️ 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).
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
🗒️ 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=
[...] 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'],
}
// (...)
}
📤 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;
A ce stade, le bouton « Se connecter via LinkedIn » nous redirige vers le portail d'authentification LinkedIn :
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 :
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');
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;
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 });
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;
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 choisiauthorization_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
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=
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,
}
});
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 {}
[...] 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();
🎉 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
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);
// ...
}
}
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);
}
}
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 {}
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 {}
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' },
});
}
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.
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;
}
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!');
}
}
}
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;
}
}
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' },
});
}
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;
}
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,
}
}
}
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;
}
}
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.
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
October 17, 2023