Le rôle du Développeur dans l'équipe produit
Le Développeur a une position clé au sein de l'équipe produit. Comment évolue-t-elle ? Charles vous dit tout 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.
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.
Albert est notre nouvel employé pour lequel nous venons de créer un compte Google. Il veut se connecter à notre application interne SpaceY.
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
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 :
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 :
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'); }
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
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 :
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 :
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.
Nous croyons en un nouveau modèle de consulting où l’excellence commence par l’écoute, le partage et une vraie vision