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 : 

  • le resource owner : l’utilisateur de l’application (vous ou moi)
  • l’application client : notre application Svelte
  • le resource server : notre API Quarkus
  • l’authorization server : le site https://auth0.com/

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 :

  • À l’arrivée sur notre application, notre utilisateur, encore anonyme, pourra consulter les produits de notre catalogue. 
  • Un lien permettra à l’utilisateur de s’authentifier.
  • Une fois authentifié, et selon les autorisations qui lui auront été attribuées, il pourra : 
    • ajouter des produits dans son panier, 
    • consulter son panier, 
    • et/ou supprimer des produits de son panier.

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

  • La version non sécurisée de l’application se trouve dans la branche main.
  • La version sécurisée de l’application se trouve dans la branche feat/securing.

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 :

  • Allowed Callback URLs
  • Allowed Logout URLs
  • Allowed Web Origins

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 :

  • read:cart pour autoriser l’affichage du Cart
  • add:cart pour l’affichage et l’utilisation du bouton Add To Cart
  • delete:cart pour l’affichage et l’utilisation du bouton Remove From Cart

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 :

  • Read Cart que l’on associera à la permission read:cart
  • Add to Cart que l’on associera à la permission add:cart
  • Delete from Cart que l’on associera à la permission delete:cart

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);
  • La variable auth0Client va nous servir à communiquer avec auth0.
  • La variable isAuthenticated sert à savoir si l’utilisateur est déjà connecté.
  • La variable user sert à accéder aux infos de l’utilisateur.
  • La variable popupOpen sert à savoir si la popup Login / Mot de passe est affichée.

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 :

  • Dans la partie <script></script> tout en haut du fichier nous allons ajouter les quelques fonctions suivantes :
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.

  • Dans la partie HTML main, on complète le bandeau (ligne 43) :
<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 :

  • nous allons importer la variable isAuthenticated du store :
import { isAuthenticated } from "../store";
  • et faire en sorte de n’afficher le bouton Add To Cart que quand notre utilisateur est identifié en conditionnant son affichage par la variable isAuthenticated :
{#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) :

  • on importe les variables du store :
import {isAuthenticated, auth0Client} from "../store";
  • et modifions la fonction apiRequest ainsi :
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 :

  • ajout de la dépendance à quarkus-oidc dans le cart-service/pom.xml :
<dependency>
 <groupId>io.quarkus</groupId>
 <artifactId>quarkus-oidc</artifactId>
</dependency>
  • ajout des paramètres de configuration dans le fichier cart-service/src/main/resources/application.properties :
    • auth-server-url : c’est l’URL de votre compte auth0
    • client-id : le client ID récupéré lors de la création de votre application SPA
    • roles-claim-path : c’est le path du noeud JSon dans le Token où se trouve les informations de rôle que l’on souhaite utiliser. Ici auth0 fournis les rôles des utilisateurs dans la propriété permission
    • audience : information que l’on a donné à la creation de l’API Backend auth0
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/
  • Dans le fichier cart-service/src/main/java/org/sixdouglas/quarkus/cart/CartResource.java

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;
}
  • enfin nous ajoutons l’annotation @RolesAllowed sur les méthodes de notre CartResource

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 :

  • pour les API Quarkus :
./mvnw compile quarkus:dev
  • pour la partie front Svelte :
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

2 réflexions sur “Sécurisation OAuth2 d’une Single Page Application

  • 15 juillet 2021 à 6 h 36 min
    Permalien

    J’utilise différentes application au quotidien mais je ne savais pas que ces dernières nécessitaient une sécurisation spécifique. C’est donc grâce à cet article de blog que j’ai pu prendre connaissance de l’importance de cette protection avec OAuth. Un article qui m’a permis d’éviter les soucis difficiles à régler !

    Répondre
    • 19 juillet 2021 à 17 h 36 min
      Permalien

      Content que cet article ai pu t’aider.

      Répondre

Répondre à Victoria Annuler la réponse.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :