Cours NestJS · Module 4: Authentification
OAuth et Social Login - les alliances de l'Empire
Dans cette leçon5
Un citoyen de la province de Gaule se présente à la porte du camp. Tu peux lui imposer de créer un nouveau compte : inventer un mot de passe, confirmer une adresse, suivre toute la procédure de recrutement. Ou bien conclure une alliance avec la Gaule : ses fonctionnaires confirmeront l'identité du nouveau venu, et tu accepteras cette confirmation comme la tienne.
Cette seconde voie s'appelle OAuth 2.0 - et c'est elle qui se cache derrière le bouton "Se connecter avec Google" que tu connais de centaines de sites. On l'enseigne à cet endroit pour une raison : le mot de passe du citoyen n'entre jamais dans ton camp. Tu n'as pas à le stocker, le hacher ni le protéger d'une fuite, puisque tu ne le vois jamais.
Les quatre étapes de l'alliance
Avant de regarder du code, suivons ce qui se passe à ce bouton - car cette séquence est le cœur d'OAuth, et le code ne fait que la servir.
- Le citoyen clique sur "Se connecter avec Google" sur ta page.
- Ton application le redirige vers Google. À partir de cet instant, il parle à Google et non à toi - et c'est là, sur la page de Google, qu'il saisit son mot de passe.
- Google le renvoie vers une adresse convenue, en y joignant un code à usage unique. Ton serveur échange ce code contre les données du citoyen.
- Ton application délivre son propre JWT - désormais le citoyen circule dans le camp avec ton laissez-passer, et le rôle de Google s'achève.
L'étape quatre est celle qu'on oublie le plus souvent. Google confirme l'identité une seule fois, à l'entrée. Tout le travail ultérieur de l'application repose sur ton propre laissez-passer - exactement celui que tu as rencontré dans la leçon sur JWT.
La stratégie - traiter le retour de Gaule
Le code de cet échange est pris en charge par une stratégie Passport. Tu en connais le squelette ; seul le corps de validate est nouveau :
1@Injectable()
2export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
3 constructor(
4 private configService: ConfigService,
5 private usersService: UsersService,
6 ) {
7 super({
8 clientID: configService.get('GOOGLE_CLIENT_ID'),
9 clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
10 callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
11 scope: ['email', 'profile'],
12 });
13 }
14
15 async validate(accessToken: string, refreshToken: string, profile: any) {
16 const { name, emails } = profile;
17
18 return this.usersService.findOrCreateSocialUser({
19 email: emails[0].value,
20 firstName: name.givenName,
21 provider: 'google',
22 providerId: profile.id,
23 });
24 }
25}Dans super(), nous décrivons l'alliance elle-même. clientID et clientSecret sont les papiers de ton application délivrés par Google - lus depuis la configuration, jamais écrits dans le code. callbackURL est l'adresse de l'étape trois, vers laquelle Google renverra le citoyen ; tu dois déclarer cette même adresse dans la console Google, faute de quoi la redirection sera rejetée. scope précise ce que tu demandes - ici seulement une adresse e-mail et un profil de base.
La règle concernant scope est brève : demande le minimum. Chaque permission supplémentaire est un écran de consentement de plus pour l'utilisateur et une chose de plus à protéger.
La méthode validate ne s'exécute qu'après le retour de Google, avec le profil du citoyen déjà prêt. L'objet retourné atterrit - comme dans toute stratégie - dans request.user.
Deux identifiants à la place d'un mot de passe
Note la paire provider et providerId dans les données que nous transmettons. C'est elle qui remplace le mot de passe.
providerId est l'identifiant du citoyen dans le système de Google - immuable, contrairement à l'adresse e-mail, qui peut être modifiée. Mais providerId seul ne suffit pas, car un citoyen portant le même numéro peut exister chez un autre fournisseur. Nous retenons donc la paire : qui a confirmé et qui a été confirmé.
1async findOrCreateSocialUser(data: SocialUserDto): Promise<User> {
2 const existing = await this.usersRepository.findOne({
3 where: { provider: data.provider, providerId: data.providerId },
4 });
5
6 if (existing) {
7 return existing;
8 }
9
10 return this.usersRepository.save({
11 email: data.email,
12 firstName: data.firstName,
13 provider: data.provider,
14 providerId: data.providerId,
15 });
16}Le nom findOrCreate décrit toute la logique : à la première connexion nous créons l'entrée, à chaque suivante nous la retrouvons. Note ce qui manque à cette entité - une colonne de mot de passe. Un citoyen admis par l'alliance n'a jamais fourni de mot de passe, il n'y a donc rien à enregistrer. C'est d'ailleurs un piège facile : si ta colonne password est obligatoire (nullable: false), l'enregistrement d'un tel utilisateur échouera.
Deux endpoints qui referment la boucle
Côté contrôleur, l'alliance se résume à deux adresses :
1@Controller('auth')
2export class AuthController {
3 @Get('google')
4 @UseGuards(AuthGuard('google'))
5 googleLogin() {
6 // la redirection est effectuée par le guard
7 }
8
9 @Get('google/callback')
10 @UseGuards(AuthGuard('google'))
11 googleCallback(@Request() req) {
12 return this.authService.generateToken(req.user);
13 }
14}Le premier endpoint a un corps vide, et ce n'est pas une erreur - son unique rôle est de déclencher le guard, qui redirige l'utilisateur vers Google. Du code placé dans cette méthode ne s'exécuterait jamais.
Le second est l'adresse de callbackURL. Quand le citoyen revient, le guard échange le code contre un profil, la stratégie enregistre ou retrouve l'utilisateur, et nous délivrons à la fin notre propre JWT. C'est ici que se referme l'étape quatre : à partir de cet instant, l'application n'a plus besoin de Google.
Récapitulatif
L'alliance est conclue, la porte est ouverte aux citoyens des provinces voisines :
- OAuth 2.0 permet de se connecter via un service externe, et le mot de passe n'entre jamais dans ton application,
- le flux compte quatre étapes : le clic, la redirection vers le fournisseur, le retour avec un code, la délivrance de ton propre JWT,
- le rôle du fournisseur s'achève après la connexion - c'est ensuite ton laissez-passer qui prend le relais,
clientIDetclientSecretse lisent depuis la configuration,callbackURLdoit correspondre à l'adresse déclarée chez le fournisseur,- dans
scope, demande le minimum de données nécessaires, - tu retiens l'identité sous forme de paire
provider+providerId- elle remplace le mot de passe, car les e-mails changent, findOrCreatecrée l'entrée à la première connexion et la retrouve à chacune des suivantes,- un utilisateur OAuth n'a pas de mot de passe, la colonne
passwordne peut donc pas être obligatoire, - l'endpoint de départ a un corps vide - tout le travail est fait par le guard.
Dans la prochaine leçon, nous aborderons les sessions et les cookies, l'autre route vers le fort - l'alternative avec état aux laissez-passer. En attendant, retiens ceci : OAuth est une alliance où un voisin se porte garant du nouveau venu ; tu acceptes cette garantie une fois et délivres aussitôt un document bien à toi.
Code de cette leçon : src/auth/google.strategy.ts
1// OAuth et Social Login - les alliances de l'Imperium
2import { Injectable } from '@nestjs/common';
3import { PassportStrategy } from '@nestjs/passport';
4import { Strategy, VerifyCallback } from 'passport-google-oauth20';
5import { ConfigService } from '@nestjs/config';
6
7// TODO: Implémente GoogleStrategy
8// 1. Étends PassportStrategy(Strategy, 'google')
9// 2. Configure clientID, clientSecret, callbackURL, scope
10// 3. Implémente validate() pour traiter le profil Google
11@Injectable()
12export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
13 constructor(private configService: ConfigService) {
14 super({
15 // TODO: Récupère clientID depuis configService
16 clientID: '',
17
18 // TODO: Récupère clientSecret depuis configService
19 clientSecret: '',
20
21 // URL de retour après la connexion Google
22 callbackURL: 'http://localhost:4000/auth/google/callback',
23
24 // Portées (scopes) de données à récupérer
25 scope: ['email', 'profile'],
26 });
27 }
28
29 // TODO: Implémente validate
30 async validate(
31 accessToken: string,
32 refreshToken: string,
33 profile: any,
34 done: VerifyCallback,
35 ): Promise<any> {
36 const { name, emails, photos } = profile;
37
38 // TODO: Crée l'objet citoyen à partir des données Google
39 const citizen = {
40 email: '', // TODO: emails[0].value
41 firstName: '', // TODO: name.givenName
42 lastName: '', // TODO: name.familyName
43 picture: '', // TODO: photos[0].value
44 provider: 'google',
45 accessToken,
46 };
47
48 // TODO: Call done(null, citizen)
49 done(null, citizen);
50 }
51}
52
53// Contrôleur OAuth
54import { Controller, Get, UseGuards, Request } from '@nestjs/common';
55import { AuthGuard } from '@nestjs/passport';
56
57@Controller('auth')
58export class OAuthController {
59 // Redirection vers Google
60 @Get('google')
61 @UseGuards(AuthGuard('google'))
62 async googleAuth() {
63 // Passport redirigera automatiquement vers Google
64 }
65
66 // Callback après connexion
67 @Get('google/callback')
68 @UseGuards(AuthGuard('google'))
69 googleAuthCallback(@Request() req: any) {
70 // req.user contient les données de validate()
71 return { message: 'Le citoyen a rejoint via Google !', user: req.user };
72 }
73}
74
75console.log('OAuth 2.0 - connexion via des services externes');
76console.log('Flux : App -> Google -> Callback -> Token JWT');
77Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que OAuth 2.0 permet dans une application web ?
Tâches pratiques dans le jeu
- Tri vertical
Placez les étapes de la connexion OAuth :