Blog ENI : Toute la veille numérique !
💥 Un livre PAPIER acheté
= La version EN LIGNE offerte pendant 1 an !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. OAuth 2 et OpenID Connect
  3. Protéger une API Web
Extrait - OAuth 2 et OpenID Connect Sécurisez vos applications .Net avec IdentityServer
Extraits du livre
OAuth 2 et OpenID Connect Sécurisez vos applications .Net avec IdentityServer Revenir à la page d'achat du livre

Protéger une API Web

Introduction

La protection d’une API Web peut se faire de différentes manières : par une API Key, une basic auth, ou par token. Je mets volontairement de côté les cookies qui ne sont pas adaptés à ce cas de figure. Dans ce chapitre, nous aborderons ces différentes méthodes, mais également les différentes manières de contrôler les droits d’accès. L’accès par scope, par rôle, etc. À la fin de ce chapitre, vous saurez tout sur la manière de protéger efficacement vos API.

Le cas de la basic auth ne sera pas traité car il est vraiment très basique et présente de nombreux inconvénients en termes de sécurité. Les tokens JWT sont un moyen bien plus fiable de donner un accès à un utilisateur. Pour un accès simplifié qui peut être partagé ou non entre différents utilisateurs, l’API Key remplit parfaitement le rôle et est plus simple à utiliser.

Créer une API

Dans la solution Visual Studio, ajoutez un nouveau projet de type Web API  :

images/11EI001.png
images/11EI002.png

Lors de la création du projet, la case à cocher Enable OpenAPI support induit l’ajout de Swashbuckle à votre projet. Swashbuckle est une librairie qui apporte le support de Swagger (Open API) aux applications .Net.

Dans cette API, nous allons ajouter un endpoint pour récupérer une liste de livres.

Commençons par créer le modèle de données. Ajoutez un dossier Models à la solution.

Dans ce dossier, nous allons ajouter une classe Book :

public class Book  
{  
    public Guid Id { get; set; }  
    public string Title { get; set; } = string.Empty;  
    public string Description { get; set; } = string.Empty;  
    public string Author { get; set; } = string.Empty;  
  
} 

Ajoutez ensuite un contrôleur BooksController :

[ApiController]  
[Produces(MediaTypeNames.Application.Json)]  
[Route("api/books")]  
public class BooksController : ControllerBase  
{  
  [HttpGet]  
  public IActionResult Get()  
  {  
    Book[] books = new Book[]  
    {  
      new()...

Autoriser les accès à l’API

Nous allons maintenant restreindre les accès à cette API en exigeant que l’appelant soit un utilisateur authentifié. Il faudra donc fournir un access token pour accéder aux ressources de l’API.

1. Accès par JWT

Par défaut, les projets .Net ne prennent pas en charge l’authentification/autorisation via un token JWT. Il faut installer un package Nuget spécifique :

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer 

Dans le fichier Program.cs, il faudra ajouter les services d’authentification :

builder.Services.AddAuthentication()  
  .AddJwtBearer(options =>  
  {  
    options.Authority = "https://localhost:5000";  
    options.TokenValidationParameters.ValidateAudience = false; 
    options.TokenValidationParameters.ValidTypes = new string[] { "at+jwt" };  
}); 

Et dans la configuration du pipeline :

app.MapControllers().RequireAuthorization(); 

Afin que tous les contrôleurs nécessitent une authentification. Il y a d’autres moyens d’obtenir le même résultat, mais celui-ci à l’avantage d’être global et de ne rien laisser passer.

Relancez l’API et retentez l’appel via Swagger vous obtiendrez une erreur 401.

L’API est maintenant protégée. C’est très bien mais nous ne pouvons plus y accéder. Il nous faut un token que nous pourrons passer dans nos appels API.

Plusieurs solutions s’offrent à vous pour cela :

1. Vous utilisez les informations de votre application MVC pour demander un token. Cela fonctionne, mais si vous voulez utiliser des scopes spécifiques pour tester des autorisations, cela peut être problématique.

2. Vous créez un client dédié, fictif, qui ne servira qu’à des fins de tests.

Nous allons opter pour la solution du client fictif dédié au tests. De cette manière, chaque API peut disposer d’un client dédié utilisable ou non en environnement de production. Ceci est très pratique pour le développement, bien sûr, mais également pour que des clients puissent tester une API.

Ensuite, il existe plusieurs manières de demander...

Autorisations par scope

1. Configuration des règles dans l’API

Actuellement, tout utilisateur ayant un token valide peut accéder à notre API. Nous allons donc restreindre cet accès aux seuls porteurs d’un token contenant le scope books.read.

a. La méthode globale

Comme nous avons demandé de manière globale à ce que tous nos contrôleurs soient protégés par une autorisation, nous pouvons également demander à ce que de manière globale soient appliquées certaines restrictions.

La méthode d’extension .RequireAuthorization() dispose de cinq signatures :

public static TBuilder RequireAuthorization<TBuilder>(this  
TBuilder builder)  
  
public static TBuilder RequireAuthorization<TBuilder>(this  
TBuilder builder, params string[] policyNames)  
  
public static TBuilder RequireAuthorization<TBuilder>(this  
Builder builder, params IAuthorizeData[] authorizeData)  
  
public static TBuilder RequireAuthorization<TBuilder>(this  
Builder builder, AuthorizationPolicy policy)  
  
public static TBuilder RequireAuthorization<TBuilder>(this  
TBuilder builder, Action<AuthorizationPolicyBuilder> configurePolicy) 

Nous remarquons donc, avec les signatures, que nous pouvons restreindre l’autorisation via des IAuthorizeData ou via des AuthorizationPolicy.

L’interface IAuthorizeData expose trois propriétés :

public interface IAuthorizeData  
{  
  string? Policy { get; set; }  
  
  string? Roles { get; set; }  
  
  string? AuthenticationSchemes { get; set; }  
} 

Le nom d’une police d’autorisation, qui doit avoir été définie au préalable, des rôles sous forme d’un string ou chaque rôle est séparé par une virgule et enfin une liste de schemes également sous forme d’un string où chaque scheme est séparé par une virgule. Ces trois paramètres sont ceux que l’on retrouve sur le décorateur [Authorize] qui hérite lui-même de IAuthorizeData.

Si vous regardez les cinq implémentations de la méthode...

Role Based Access Control

L’accès par scope, comme évoqué précédemment, présente un inconvénient majeur : le scope est attribué à l’application cliente indépendamment de l’utilisateur qui fait la demande. Or, on pourrait tout à fait désirer restreindre l’accès à des utilisateurs n’ayant pas des privilèges suffisants. Une des possibilités est d’utiliser un claim, rôle, afin de restreindre l’accès de l’utilisateur.

1. Scope "roles"

Nous allons ajouter à nos ressources utilisateur disponibles une ressource roles qui contiendra un claim role.

Dans la méthode d’initialisation des données, à la suite des IdentityResource déjà présentes ajoutons :

new IdentityResource  
{  
  Name= "roles",  
  DisplayName="roles",  
  Description="Roles scope",  
  UserClaims=new string []  
  {  
    "role"  
  }  
} 

Ensuite, dans la liste des scopes autorisés pour le client test Swagger, ajoutez roles.

Il nous faut maintenant un utilisateur qui possède un rôle particulier pour nos tests. Nous allons lui affecter le rôle Administrator.

À la fin de la méthode d’initialisation des données, juste avant le bloc try/catch de la sauvegarde, ajoutez :

if (!await applicationDbContext.Users.AnyAsync())  
{  
  ApplicationUser user = new("user@exemple.com");  
  user.EmailConfirmed = true;  
  UserManager<ApplicationUser>...

Contrôle d’accès local

Dans l’exemple précédent, nous avons utilisé la logique des access token afin de donner un accès spécifique à l’utilisateur. Il est toutefois préférable de ne pas avoir recours à cette technique et d’utiliser un contrôle d’accès « local ». Dans ce cas, c’est l’API qui, elle, connaît son métier et peut autoriser ou refuser l’accès à un utilisateur en fonction de ses règles métier. On revient au schéma où les responsabilités sont bien séparées entre une authentification et des droits métier.

1. Le cycle de vie d’un contrôleur

Dans une application ASP.Net, la requête http passe par tout un pipeline de middlewares qui se termine par l’arrivée dans le contrôleur.

IMAGES/11EI008.png

Source Microsoft

On voit sur le schéma suivant que la requête passe en premier par le AuthorizationFilter pour vérifier les droits d’accès. Le décorateur [Authorize] a pour vocation de déclencher ce filtre qui sinon laissera passer la requête au filtre suivant sans rien faire.

Nous allons donc créer notre propre AuthorizationFilter qui se basera sur les informations de l’utilisateur.

2. RoleBasedAuthorizationFilter

Dans l’API, créez un répertoire...

API Keys

La dernière méthode que nous allons étudier pour limiter l’accès à une API est l’utilisation d’une API Key. Une API Key s’apparente à un reference token. C’est une clé qui ne porte pas d’information en soi et qui permettra de valider l’accès à une ressource pour un utilisateur ou une application. Sa durée de vie peut être limitée dans le temps ou illimitée. L’API Key est un mode de fonctionnement totalement en marge du protocole OAuth2. Elle est bien plus facile à mettre en œuvre pour l’utilisateur final, mais cette simplicité se paie par une vulnérabilité plus importante.

Les API Key sont des clés figées qui ont des durées de vie longues, plusieurs mois, quand elles n’ont pas une durée de vie infinie. Il n’y a pas de mécanique d’expiration/rafraîchissement comme avec les Tokens OAuth2. La mise en œuvre est également plus simple : on génère une API Key, et elle peut servir directement dans nos requêtes via un header spécifique. Il n’y a pas de mécanique complexe de demande de tokens.

Les API Keys ressemblent à bien des égards à des Personal Access Token de type reference. Leur mise en œuvre sera un peu plus complexe car ne reposant pas du tout sur OAuth, l’implémentation devra être faite manuellement. Néanmoins, c’est une mécanique qui reste toujours très utilisée pour des services qui communiquent principalement avec des machines. Les plateformes d’envoi d’e-mail marketing en sont un bon exemple.

1. Filtres et attributs

La norme Open API précise que l’API key peut être transmise via l’URL dans le paramètre api_key, dans un header x-api-key ou dans un cookie x-api-key (https://swagger.io/docs/specification/authentication/api-keys/).

Nous nous concentrerons ici sur le paramètre d’URL et le header. Ce sont les mécaniques les plus faciles à mettre en œuvre, quel que soit le scénario.

Pour ce faire, nous allons reprendre la méthode de l’attribut couplé à son filtre d’autorisation.

Dans le répertoire Filters de l’API, créez...