Site icon Blog Zenika

Sécurisation OAuth2 d’une Single Page Application

But

Nous allons voir dans cet article comment sécuriser une application (écrite avec le framework Svelte pour le front-end, Quarkus pour les API back-end) avec une authentification / autorisation OAuth2 gérée par le site https://auth0.com/.

Définition

D’après Wikipedia OAuth c’est :

OAuth est un protocole libre qui permet d’autoriser un site web, un logiciel ou une application (dite « consommateur ») à utiliser l’API sécurisée d’un autre site web (dit « fournisseur ») pour le compte d’un utilisateur. OAuth n’est pas un protocole d’authentification, mais de « délégation d’autorisation ».

OAuth dans sa version 2.0 repose sur des échanges entre quatre acteurs. Le resource owner (utilisateur) est capable d’accorder l’accès à la ressource pour une application client. L’authorization server (serveur d’autorisation) occupe le rôle central au sein du protocole, il est chargé d’authentifier le resource owner et de délivrer son autorisation sous la forme d’un jeton appelé access token. Le resource server quant à lui correspond au serveur où sont stockées les ressources protégées.

Dans notre cas : 

Présentation

L’application que nous allons sécuriser est un site e-commerce basique : un catalogue, et un panier. Actuellement, toutes les fonctionnalités sont accessibles à tout le monde. Nous allons donc modifier ces applications pour obtenir ceci :

Le code de cette application est disponible sur Github : https://github.com/sixdouglas/securing-spa

Voici à quoi elle ressemble :

Nous allons créer nos utilisateurs, gérer leurs mots de passe et leur attribuer des droits via l’interface d’administration de https://auth0.com/.

Création et paramétrage du compte auth0

Vous pouvez vous connecter avec votre compte Github ou Google (c’est aussi une application de l’OAuth2 !), ou alors utiliser une adresse email et un mot de passe.

Application Frontend

Pour l’application Svelte, nous voulons une mire d’authentification (login / mot de passe). Pour cela, une fois connecté, il vous faudra créer une application de type Single Page Application. Pour l’exercice nous l’appellerons “Secured Cart” :

Puis, comme Svelte n’est pas proposé parmi les templates, nous choisissons l’option JavaScript :

Nous arrivons ensuite sur la page Quick Start de notre application

Nous allons ensuite configurer notre “Secured Cart”. Dans l’onglet Settings, vous trouverez le Domain et le Client ID dont vous aurez besoin ultérieurement.

Dans les champs ci-dessous, il vous faut saisir l’URL de votre web app http://localhost:5000 :

Ainsi, il vous sera possible de vous connecter en revenant vers votre application.

API Backend

Pour protéger vos API Quarkus avec des permissions, vous allez devoir créer une API en lui donnant un nom et un identifiant. Auth0 recommande que l’identifiant soit sous forme d’URL :

Auth0 vous propose quelques exemples de code pour utiliser votre API, mais pour le moment, rien en Java.
Dans les settings de votre API, récupérez l’Identifiant. Nous l’utiliserons en tant qu’audience dans les futurs appels à auth0 :

Un peu plus bas dans la page, vous devez activer le RBAC et activer l’option d’ajout des permissions dans le Token, sans oublier de sauvegarder ensuite :

Nous allons maintenant ajouter les permissions nécessaires à notre application. C’est dans l’onglet Permission que cela se passe :

Il nous faut trois permissions différentes :

Il nous reste maintenant à configurer notre API pour qu’elle fonctionne avec l’Application créée précédemment :

Attention

Il ne faut pas cocher les permissions dans cet écran, autrement elles seront automatiquement ajoutées à celles de nos utilisateurs à chacune des requêtes et ce n’est pas ce que l’on souhaite, puisque nous voulons pouvoir attribuer des droits différents selon l’utilisateur.

Gestion des permissions

Gestion des rôles

Pour la gestion des rôles, il nous faut aller dans la partie User Management, et sélectionner Rôles dans le sous-menu. Nous allons créer trois rôles différents, un par permission :

Pour créer un rôle, il faut lui donner un nom et une description et cliquer sur Create :

Puis, dans l’onglet Permissions :

Il faut ajouter les permissions que l’on veut attribuer aux utilisateurs de ce groupe. Pour cela, on sélectionne l’API qui nous intéresse :

Puis les permissions de cette API et clique sur Ajouter :

Il faut donc répéter l’opération pour chacune des permissions en lui créant son rôle.

Gestion des utilisateurs

Pour les utilisateurs, dans le menu User Management, on sélectionne le sous-élément Users. Pour ajouter un utilisateur, il vous faut une adresse e-mail et un mot de passe.

Astuce

Pour ceux qui possèdent un compte GMail :
À partir d’une adresse de base, par exemple monnom@gmail.com
GMail crée des alias à cette boite sous la forme :
monnom-01@gmail.com
monnom-02@gmail.com

monnom-05@gmail.com
Pour GMail, c’est alors la même boîte mail, mais la plupart des sites, considèrent ces adresses comme des adresses différentes. Vous pouvez donc créer vos différents utilisateurs auth0 en utilisant ce principe.

Une fois l’utilisateur créé, il faut lui ajouter le ou les rôles que l’on souhaite. C’est dans l’onglet Rôle que cela se passe en cliquant sur Assign Role :

Bien sûr, vous êtes libre de choisir les rôles que vous attribuez à vos utilisateurs et, une fois le Tuto réalisé, je vous invite à jouer avec afin de vérifier que le comportement est bien celui que vous souhaitez.

Sécurisation de la Web App

Lien avec auth0

La première étape consiste à ajouter la dépendance npm vers la bibliothèque auth0 :

npm install @auth0/auth0-spa-js

Puis d’ajouter cinq variables dans le store ( cart-front/src/store/index.js ) :

export const auth0Client = writable({});

export const isAuthenticated = writable(false);
export const user = writable({});
export const popupOpen = writable(false);

REM : L’objet auth0Client, maintient en interne un cache pour le token. Il se charge également de le renouveler quand il a expiré. Nous allons donc y faire appel à chaque fois que nous avons besoin du token. Le sauvegarder dans le store permet d’utiliser toujours le même et le rend accessible de partout dans l’application.

Il nous faut ensuite créer un fichier de configuration ( cart-front/src/security/authConfig.js ) avec les paramètres de auth0 :

const config = {
   domain: "dev-jd008hz4.eu.auth0.com",
   clientId: "Hxsrhl********************GA0NV1",
   audience: "https://reactive-app/"
};

export default config;

Ces informations sont celles que l’on a récupérées dans les écrans de auth0, domain et clientId lors de la création de l’Application, l’audience lors de la création de l’API.

Puis nous allons créer un service qui se chargera de l’authentification ( cart-front/src/security/authService.js )

import createAuth0Client from "@auth0/auth0-spa-js";
import { user, isAuthenticated, token, popupOpen } from "../store";
import config from "./authConfig";

function createClient() {
   return createAuth0Client({
       domain: config.domain,
       client_id: config.clientId,
       audience: config.audience
   });
}

async function loginWithPopup(client, options) {
   popupOpen.set(true);
   try {
       await client.loginWithPopup(options);

       user.set(await client.getUser());
       isAuthenticated.set(true);
   } catch (e) {
       console.error(e);
   } finally {
       popupOpen.set(false);
   }
}

async function getToken(client, options) {
   try {
       return await client.getTokenSilently(options);
   } catch (e) {
       console.error(e);
   }
}

function logout(client) {
   client.logout();
   user.set({});
   isAuthenticated.set(false);
}

const auth = {
   createClient,
   loginWithPopup,
   getToken,
   logout
};

export default auth;

La fonction createClient() va nous permettre d’instancier le client auth0.
La fonction loginWithPopup() va déclencher l’ouverture de la popup pour saisir son login / mot de passe.
La fonction getToken() permet de récupérer un token JWT que l’on utilisera pour les appels API.
La fonction logout() est plutôt explicite 😉

Ajout de la mire de connexion

Nous allons commencer par modifier le fichier principal de l’application pour ajouter l’appel à la mire de connexion et le bouton de déconnexion. Modifions donc le fichier cart-front/src/App.svelte :

import { onMount } from "svelte";
import auth from "./security/authService";
import { isAuthenticated, user, auth0Client } from "./store";

let auth0ClientPromise = auth.createClient();
let auth0Cli;

onMount(async () => {
  auth0ClientPromise
        .then(auth0ClientValue => {
           auth0Cli = auth0ClientValue;
           $auth0Client = auth0ClientValue;
           $auth0Client.isAuthenticated()
                 .then(isAuthenticatedValue => isAuthenticated.set(isAuthenticatedValue));
           $auth0Client.getUser()
                 .then(userValue => user.set(userValue));
        });
});

function login() {
  auth.loginWithPopup(auth0Cli);
}

function logout() {
  auth.logout(auth0Cli);
}

Sur l’étape mount du cycle de vie des composants Svelte, nous instancions le client auth0 et le stockons dans le store, nous attribuons une valeur initiale aux propriétés isAuthenticated et user du store. Ensuite, deux fonctions qui appellent celles du service authService. Elles seront appelées depuis les liens Log In, Log Out du bandeau.

<div class="navbar-nav mr-auto user-details"></div>
<div class="navbar-text"></div>

Comme ceci :

<div class="navbar-nav mr-auto user-details">
  {#if $isAuthenticated}
     <span class="text-white">&nbsp;&nbsp;{$user.name} ({$user.email})</span>
  {:else}
     <span>&nbsp;</span>
  {/if}
</div>
<div class="navbar-text">
  <ul class="navbar-nav float-right">
     {#if $isAuthenticated}
        <li class="nav-item">
           <a class="nav-link" href="/#" on:click="{logout}">Log Out</a>
        </li>
     {:else}
        <li class="nav-item">
           <a class="nav-link" href="/#" on:click="{login}">Log In</a>
        </li>
     {/if}
  </ul>
</div>

Nous aurons ainsi le nom de la personne connectée dans la barre de titre de la page, et alternativement un bouton pour se connecter ou un bouton pour se déconnecter, selon que l’utilisateur est déjà connecté ou non.

Conditionnement de l’affichage du panier et du bouton Add to cart

Restons dans le fichier cart-front/src/App.svelte, nous allons également protéger l’affichage du panier, pour que celui-ci ne soit visible que lorsque notre utilisateur est authentifié  :

{#if $isAuthenticated}
<div class="col-3 border-left">
  <Cart />
</div>
{/if}

Puis dans le fichier cart-front/src/catalog/Product.svelte :

import { isAuthenticated } from "../store";
{#if $isAuthenticated}
<span class="btn btn-warning" on:click="{addToCart(product.id)}">Add to cart</span>
{/if}

Ajout d’un token JWT lors des appels aux APIs

Pour que les appels aux API soient effectués avec le token, il nous reste à modifier le helper d’appel aux APIs (cart-front/src/api/index.js) :

import {isAuthenticated, auth0Client} from "../store";
function apiRequest(method, url, request) {
   console.info("call API: '" + url + "' in '" + method + "', with params: {" + request + "}");
   let headers = {};
   let isAuthent;
   isAuthenticated.subscribe(value => isAuthent = value);
   let auth0Cli;
   auth0Client.subscribe(value => auth0Cli = value);
   if (isAuthent) {
       return auth0Cli.getTokenSilently()
           .then( token => {
               headers = {
                   authorization: "Bearer " + token
               }
               return callApi(method, url, request, headers);
           });
   } else {
       return callApi(method, url, request, headers);
   }
}

Voilà, c’est tout pour la partie Svelte. Passons aux APIs.

Sécurisation des APIs

Pour la partie Quarkus, nous allons configurer la couche de sécurité Quarkus avec un flow OAuth2 de type Autorization Flow qui va authentifier l’utilisateur depuis le token JWT envoyé depuis le front via le header Authorization.

L’extension Quarkus OIDC va être utilisée pour faire cela.

Nous pourrons ensuite gérer les accès et les habilitations soit via fichier de properties ou via les annotations @RolesAllowed ou @Authenticated.

Je vous propose d’utiliser les annotations @RolesAllowed

Il nous faut donc ajouter une dépendance, 4 informations de configuration et une annotation sur les Endpoint que l’on souhaite protéger.

Voici les modifications :

<dependency>
 <groupId>io.quarkus</groupId>
 <artifactId>quarkus-oidc</artifactId>
</dependency>
quarkus.oidc.auth-server-url=https://dev-jd008hz4.eu.auth0.com
quarkus.oidc.client-id=Hxsrhl********************GA0NV1
quarkus.oidc.roles.role-claim-path=permissions
quarkus.oidc.token.audience=https://secured.cart.org/

Nous allons injecter le token JWT dans la classe :

@Inject
JsonWebToken jwt;

L’attribut subject est l’identifiant de l’utilisateur, nous pouvons alors l’utiliser pour récupérer son Cart :

public Cart getUserCart() {
   return Cart.findByUserId(jwt.getSubject())
           .orElseGet(this::createNewAndSaveCart);
}

Nous allons également vérifier que l’ID du panier, transmis dans les fonctions addProductToUserCart et removeProductFromUserCart, est bien celui contenu dans le token, en ajoutant l’assertion suivante dans les deux fonctions :

Assert.assertTrue(userCart.userId.equals(jwt.getSubject()));

Modifions également la fonction de création du nouveau panier pour utiliser le subject comme userId :

private Cart createNewAndSaveCart() {
   LOGGER.info("Get New Cart");
   final Cart cart = new Cart();
   cart.userId = jwt.getSubject();
   cart.persist();
   return cart;
}

pour le Endpoint GET :

@GET
@RolesAllowed("read:cart")
public Cart getUserCart() {

pour le Endpoint PUT :

@PUT
@Path("/{cartId}")
@RolesAllowed("add:cart")
public Cart addProductToUserCart(@PathParam("cartId") Long cartId, Product product) {

et enfin pour le DELETE :

@DELETE
@Path("/{cartId}/lines/{lineId}")
@RolesAllowed("delete:cart")
public Cart removeProductFromUserCart(@PathParam("cartId") Long cartId, @PathParam("lineId") Long lineId) {

Résultat

Pour visualiser nos modifications, le plus simple est de démarrer les deux applications, comme précisé dans le README.md du repo Github :

./mvnw compile quarkus:dev
npm run dev

L’application est maintenant accessible à cette URL : http://localhost:5000/#

Nous obtenons notre application, avec uniquement la liste des produits de visible car nous ne sommes pas encore identifiés :

En cliquant sur le lien Login In (en haut à droite) nous obtenons la mire de login auth0 :

Puis, l’application nous affiche toujours la liste des produits avec les boutons Add to Cart et notre panier vide sur la droite :

Nous pouvons ajouter quelques articles dans le panier :

Puis essayer de supprimer un des produits. Mais comme notre utilisateur n’a pas le droit  delete:cart nous aurons un message d’erreur 403 dans les logs :

En regardant les trames réseau, nous pouvons vérifier que le token est bien transmis à nos API :

Il est même possible de connaître le contenu exact du token en utilisant, par example, le site http://jwt.io

Liens Utiles

Auteur/Autrice

Quitter la version mobile