Aller au contenu

ASP.NET • Clef d'API

Rappelons ce qu’est une autorisation par clef d’API.

Un client (une application, un site web) a besoin d’accéder à une API hébergée sur un serveur. Si l’API n’impose pas d’autorisation alors n’importe quel client peut y accéder, sans restriction. Nous pouvons exiger du client de fournir une clef si nous souhaitons sécuriser l’API. Ainsi le client ne peut accéder à l’API que si cette clef est valide. Une clef d’API est en général une chaine de caractères, plutôt longue et complexe.

C’est une forme d’autorisation assez répandue, elle est souvent associée à une ou plusieurs autres vérifications afin d’améliorer la sécurité : adresse IP, site web (Referer), application mobile (Android ou iOS), etc.

La clef d’API peut être fournie via :

  • un paramètre dans l’URL (Query Parameter) ;
  • une valeur dans le corps de la requête (Request Body) ;
  • une valeur dans l’en-tête de la requête (Request Header).

Il est temps de mettre les mains dans le cambouis ! Commençons par créer un nouveau projet de type webapi :

Fenêtre de terminal
dotnet new webapi --use-controllers --name=ApiKeyWebApp

Nous pouvons maintenant ouvrir le projet ApiKeyWebApp dans notre IDE favori. Le projet est fonctionnel et prêt à l’emploi.

Dans le fichier de configuration, ajoutons une section ApiKeyAuthorisation qui contient :

  • le nom du paramètre utilisé ;
  • la valeur du paramètre, c’est-à-dire la clef d’API.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ApiKeyAuthorisation" : {
"Key": "UHo6CYoNGwxWFGgsfWQetDo88GUB5zjlh1srnf2rwqtaXgMayBo5AzaD8Ypy8I7R"
}
}

Créons le dossier Authorisation à la racine du projet, puis le fichier ApiKeyAuthorisationConfiguration.cs dans ce dossier :

Authorisation/ApiKeyConfiguration.cs
namespace ApiKeyWebApp.Authorisation;
public sealed record ApiKeyConfiguration
{
public required string Key { get; init; }
}

Le record ApiKeyAuthorisationConfiguration nous permettra de stocker les valeurs issues du fichier de configuration appsettings.json.

Program.cs
using ApiKeyWebApp.Authorisation;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<ApiKeyConfiguration>(
builder.Configuration.GetRequiredSection("ApiKeyAuthorisation")
);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

La méthode Configure<T>(IConfiguration config) nous permet de lier la section ApiKeyAuthorisation de la configuration au record ApiKeyConfiguration. Nous pourrons récupérer ces données en injectant IOptions<ApiKeyAuthorisation>.

Voici la structure du projet :

  • RépertoireApiKeyWebApp
    • RépertoireProperties
      • launchSettings.json
    • RépertoireAuthorisation
      • ApiKeyConfiguration.cs
    • RépertoireControllers
      • WeatherForecastController.cs
    • appsettings.json
    • appsettings.Development.json
    • Program.cs
    • WeatherForecast.cs

Une façon simple est de créer un service de vérification en charge de valider la clef d’API. Ce service pourra être utilisé dans les endpoints que nous souhaitons sécuriser.

Créons le dossier Validation sous le dossier Authorisation, puis le fichier IApiKeyValidation.cs dans ce dossier :

Authorisation/Validation/IApiKeyValidation.cs
namespace ApiKeyWebApp.Authorisation.Validation;
public interface IApiKeyValidation
{
bool IsValid(string? apiKey);
}

Il s’agit du contrat d’interface de notre service de validation. Créons ensuite l’implémentation concrète ApiKeyValidation.cs dans le même dossier :

Authorisation/Validation/ApiKeyValidation.cs
using Microsoft.Extensions.Options;
namespace ApiKeyWebApp.Authorisation.Validation;
public sealed class ApiKeyValidation(
IOptions<ApiKeyConfiguration> apiKeyConfigurationOptions
) : IApiKeyValidation
{
private readonly ApiKeyConfiguration _apiKeyConfiguration
= apiKeyConfigurationOptions.Value;
public bool IsValid(string? apiKey)
{
if (string.IsNullOrWhiteSpace(apiKey)) return false;
return _apiKeyConfiguration.Key == apiKey;
}
}

Cette nouvelle classe :

  • injecte l’option de configuration IOptions<ApiKeyConfiguration> et extrait sa valeur dans la variable _apiKeyConfiguration ;
  • hérite de l’interface IApiKeyValidation ;
  • implémente la méthode IsValid(string apiKey) ;

Cette méthode compare simplement la valeur fournie avec la valeur extraite de la configuration.

Voici la nouvelle structure de notre projet :

  • RépertoireApiKeyWebApp
    • RépertoireProperties
      • launchSettings.json
    • RépertoireAuthorisation
      • RépertoireValidation
        • ApiKeyValidation.cs
        • IApiKeyValidation.cs
      • ApiKeyConfiguration.cs
    • RépertoireControllers
      • WeatherForecastController.cs
    • appsettings.json
    • appsettings.Development.json
    • Program.cs
    • WeatherForecast.cs

Ajoutons IApiKeyValidation ↔ ApiKeyValidation dans le conteneur de services :

Program.cs
using ApiKeyWebApp.Authorisation;
using ApiKeyWebApp.Authorisation.Validation;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<ApiKeyConfiguration>(
builder.Configuration.GetRequiredSection("ApiKeyAuthorisation")
);
builder.Services.AddTransient<IApiKeyValidation, ApiKeyValidation>();
var app = builder.Build();
// ...

Nous pouvons maintenant utiliser ce service de validation dans le contrôleur :

Controller/WeatherForecastController.cs
using ApiKeyWebApp.Authorisation.Validation;
using Microsoft.AspNetCore.Mvc;
namespace ApiKeyWebApp.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
private readonly ILogger<WeatherForecastController> _logger;
private readonly IApiKeyValidation _apiKeyValidation;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
IApiKeyValidation apiKeyValidation
)
{
_logger = logger;
_apiKeyValidation = apiKeyValidation;
}
[HttpGet(Name = "GetWeatherForecast")]
publicIEnumerable<WeatherForecast> Get(string key)
public ActionResult<IEnumerable<WeatherForecast>> Get(string? key)
{
if (string.IsNullOrWhiteSpace(key)) return BadRequest();
if (!_apiKeyValidation.IsValid(key)) return Unauthorized();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
}
})
.ToArray());
}
}

Nous avons :

  • injecté IApiKeyValidation ;
  • vérifié si la clef est vide en retournant une réponse BadRequest (code HTTP 400) si c’est le cas ;
  • vérifié la validité de la clef en retournant une réponse Unauthorized (code HTTP 401) si la clef est incorrecte ;
  • retourné une réponse Success (code HTTP 200) avec le résultat si la clef est valide.

Nous pouvons vérifier le bon fonctionnement en exécutant le code du projet et en naviguant vers l’URL Swagger.

Clef d'API vide → Bad Request Clef d'API incorrecte → Unauthorized Clef d'API valide → Success

Cela fonctionne bien, mais nous devons penser à injecter le service de validation dans chaque contrôleur et valider la clef d’API manuellement pour chaque action.

Heureusement ASP.NET nous propose d’autres solutions, nous allons les détailler maintenant.

Mais avant cela, restaurons le contrôleur tel qu’il était à la création du projet :

Controller/WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
namespace ApiKeyWebApp.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

.NET nous permet de créer un attribut personnalisé (Custom Attribute). Un attribut est la décoration entre crochets [ ] que nous pouvons ajouter au-dessus d’une classe, d’une méthode ou d’une propriété.

Cet attribut peut être associé à un filtre d’action (Action Filter). Ce filtre vérifiera la validité de la clef d’API.

Voyons tout cela ensemble.

Créons le dossier Attribute sous le dossier Authorisation, puis le fichier ApiKeyAuthorizationFilter.cs dans ce dossier :

Authorisation/Attributes/ApiKeyAuthorizationFilter.cs
using ApiKeyWebApp.Authorisation.Validation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ApiKeyWebApp.Authorisation.Attribute;
public sealed class ApiKeyAuthorizationFilter(
IApiKeyValidation apiKeyValidation
) : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.Request.Query.TryGetValue("key", out var queryApiKey))
{
context.Result = new BadRequestResult();
return;
}
if (queryApiKey.Count == 0 || string.IsNullOrWhiteSpace(queryApiKey[0]))
{
context.Result = new BadRequestResult();
return;
}
if (!apiKeyValidation.IsValid(queryApiKey[0]))
{
context.Result = new UnauthorizedResult();
}
}
}

La classe ApiKeyAuthorizationFilter :

  • injecte le service de validation ApiKeyValidation que nous avons créé auparavant ;
  • hérite de IApiKeyValidation et implémente la méthode OnAuthorization(AuthorizationFilterContext context) ;
  • vérifie dans cette méthode la présence et la validité de la clef d’API issue de l’URL.

La vérification est similaire à celle que nous avons effectuée dans l’action du contrôleur :

  • clef absente, nulle ou vide → BadRequest (code HTTP 400) ;
  • clef invalide → Unauthorized (code HTTP 401) ;
  • clef valide → la requête se poursuit.

La méthode OnAuthorization() sera appelé pendant la phase d’autorisation d’une requête.

Associons le filtre d’autorisation à un attribut personnalisé :

Authorisation/Attributes/ApiKeyAuthorizationAttribute.cs
using Microsoft.AspNetCore.Mvc;
namespace ApiKeyWebApp.Authorisation.Attribute;
public sealed class ApiKeyAuthorizationAttribute()
: ServiceFilterAttribute(typeof(ApiKeyAuthorizationFilter));

La classe ApiKeyAuthorizationAttribute hérite de ServiceFilterAttribute qui permet :

  • d’associer l’attribut [ApiKeyAuthorization] au filtre ApiKeyAuthorizationFilter ;
  • d’appliquer l’attribut (et donc le filtre associé) à une action ou un contrôleur.

Voici la nouvelle structure de notre projet :

  • RépertoireApiKeyWebApp
    • Properties
    • launchSettings.json
    • RépertoireAuthorisation
      • RépertoireAttribute
        • ApiKeyAuthorizationAttribute.cs
        • ApiKeyAuthorizationFilter.cs
      • RépertoireValidation
        • ApiKeyValidation.cs
        • IApiKeyValidation.cs
      • ApiKeyConfiguration.cs
    • Controllers
    • WeatherForecastController.cs
    • appsettings.json
    • appsettings.Development.json
    • Program.cs
    • WeatherForecast.cs

Ajoutons ApiKeyAuthorizationFilter dans le conteneur de services :

Program.cs
using ApiKeyWebApp.Authorisation;
using ApiKeyWebApp.Authorisation.Attribute;
using ApiKeyWebApp.Authorisation.Validation;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<ApiKeyConfiguration>(
builder.Configuration.GetRequiredSection("ApiKeyAuthorisation")
);
builder.Services.AddTransient<IApiKeyValidation, ApiKeyValidation>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ApiKeyAuthorizationFilter>();
var app = builder.Build();
// ...

La méthode AddHttpContextAccessor() nous permet d’accéder au contexte HTTP HttpContext utilisé dans le filtre ApiKeyAuthorizationFilter.

Ajoutons le paramètre key au point à l’action Get() et l’attribut [ApiKeyAuthorization] au-dessus :

Controller/WeatherForecastController.cs
using ApiKeyWebApp.Authorisation.Attribute;
using Microsoft.AspNetCore.Mvc;
namespace ApiKeyWebApp.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
[ApiKeyAuthorization]
public IEnumerable<WeatherForecast> Get()
public IEnumerable<WeatherForecast> Get(string? key)
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

Nous pouvons vérifier le bon fonctionnement en exécutant le code du projet et en naviguant vers l’URL Swagger. L’ajout de l’attribut [ApiKeyAuthorization] permet d’obtenir le même comportement qu’avec l’implémentation manuelle du service de validation.

L’attribut personnalisé permet d’encapsuler la vérification de la clef d’API. L’action du contrôleur n’est ainsi plus en charge de cette tâche. De plus le type retourné par l’action est directement celui des données, au lieu de IActionResult ou ActionResult<T>.

Néanmoins, il faut décorer les actions ou contrôleurs que l’on souhaite protéger par clef d’API. Il est toutefois facile de créer un contrôleur de base avec l’attribut si nous souhaitons protéger toutes les routes.

Voyons maintenant une autre méthode pour vérifier la clef d’API.

Les middlewares sont un élément important d’une application ASP.NET. Ils sont utilisés dans la gestion des requêtes et des réponses. L’ensemble des middlewares constitue un pipeline que chaque requête traverse. Chaque middleware peut :

  • bloquer ou laisser passer la requête au prochain middleware ;
  • analyser et modifier la requête (et la réponse associée).

Créons le dossier Middleware sous le dossier Authorisation, puis le fichier ApiKeyAuthorizationMiddleware.cs dans ce dossier :

Authorisation/Middleware/ApiKeyAuthorizationMiddleware.cs
using System.Net;
using ApiKeyWebApp.Authorisation.Validation;
namespace ApiKeyWebApp.Authorisation.Middleware;
internal sealed class ApiKeyAuthorizationMiddleware(
RequestDelegate next,
IApiKeyValidation apiKeyValidation
)
{
public async Task InvokeAsync(HttpContext httpContext)
{
if (!httpContext.Request.Query.TryGetValue("key", out var queryApiKey))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
if (queryApiKey.Count == 0 || string.IsNullOrWhiteSpace(queryApiKey[0]))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
if (!apiKeyValidation.IsValid(queryApiKey[0]))
{
httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
await next(httpContext);
}
}

La classe ApiKeyAuthorizationMiddleware :

  • injecte le gestionnaire de requête RequestDelegate ;
  • injecte le service de validation ApiKeyValidation que nous avons créé auparavant ;
  • vérifie dans la méthode InvokeAsync(HttpContext httpContext) la présence et la validité de la clef d’API issue de l’URL.

La vérification est similaire à celles que nous avons effectuées jusqu’à présent :

  • clef absente, nulle ou vide → BadRequest (code HTTP 400) ;
  • clef invalide → Unauthorized (code HTTP 401) ;
  • clef valide → la requête est transmise au middleware suivant via next(HttpContext context).

Voici la nouvelle structure de notre projet :

  • RépertoireApiKeyWebApp
    • RépertoireProperties
      • launchSettings.json
    • RépertoireAuthorisation
      • RépertoireAttribute
        • ApiKeyAuthorizationAttribute.cs
        • ApiKeyAuthorizationFilter.cs
      • RépertoireMiddleware
        • ApiKeyAuthorizationMiddleware.cs
      • RépertoireValidation
        • ApiKeyValidation.cs
        • IApiKeyValidation.cs
      • ApiKeyConfiguration.cs
    • Controllers
    • WeatherForecastController.cs
    • appsettings.json
    • appsettings.Development.json
    • Program.cs
    • WeatherForecast.cs

Inscrivons maintenant ApiKeyAuthorizationMiddleware dans le pipeline des middlewares :

Program.cs
using ApiKeyWebApp.Authorisation;
using ApiKeyWebApp.Authorisation.Attribute;
using ApiKeyWebApp.Authorisation.Middleware;
using ApiKeyWebApp.Authorisation.Validation;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<ApiKeyConfiguration>(
builder.Configuration.GetRequiredSection("ApiKeyAuthorisation")
);
builder.Services.AddTransient<IApiKeyValidation, ApiKeyValidation>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ApiKeyAuthorizationFilter>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseMiddleware<ApiKeyAuthorizationMiddleware>();
app.UseAuthorization();
app.MapControllers();
app.Run();

Afin de tester notre middleware, supprimons l’attribut associé à l’action Get() :

Controller/WeatherForecastController.cs
using ApiKeyWebApp.Authorisation.Attribute;
using Microsoft.AspNetCore.Mvc;
namespace ApiKeyWebApp.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
[ApiKeyAuthorization]
public IEnumerable<WeatherForecast> Get()
public IEnumerable<WeatherForecast> Get(string? key)
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

En effet, les middlewares étant exécutés pour chaque requête, il n’est plus nécessaire d’ajouter d’attribut.

Nous pouvons de nouveau vérifier le bon fonctionnement en exécutant le code du projet et en naviguant vers l’URL Swagger. Comme prévu, le comportement est identique aux implémentations précédentes.

Notre middleware, comme tous les middlewares, est invoqué à chaque requête. Il n’est ainsi plus nécessaire de spécifier un attribut pour les actions et contrôleurs que nous souhaitons protéger. Aucun risque d’oubli : toutes les routes sont protégées.

Cela est bien pratique si nous souhaitons tout protéger sans exception. Mais cela l’est moins si certaines routes doivent rester accessibles sans clef d’API.

L’implémentation d’une autorisation par clef d’API a été pour nous l’occasion d’utiliser deux fonctionnalités proposées par ASP.NET :

  • les filtres ;
  • les middlewares.

Microsoft propose une documentation complète sur les mécaniques d’authentification et d’autorisation dans ASP.NET :