Loupe

Sécuriser une API Asp.Net Core avec OpenId Connect #OIDC - l'authentification par bearer token simple à mettre en place !

Sécuriser l'accès à une API est toujours un sujet délicat à mettre en place. Dans cet article nous verrons comment mettre en place OpenIdConnect sur une API ASP.Net Core et configurer son comportement.

OpenIdConnect - qu'est-ce que c'est ?

Le protocole Oauth 2.0 commence a être bien connu de nos jours et permet de gérer la notion d'autorisations dans vos applications. Cependant, il lui manque la partie identification/authentication et c'est justement ce que normalise OpenIdConnect, aussi appelé OIDC. Tout cela est basé sur REST via du HTTPS bien sûr.

Pour rappel, on distingue 2 notions :

  • Identification et authentication : qui se connecte à mon application = quelle est son identité et le processus de vérification de cette identité.
  • Authorization  : qu'est ce que peut faire un utilisateur identifié.

Le fonctionnement théorique d'OIDC est le suivant :

  1. Un client (une appli mobile, une azure function, etc.) veut accéder à une ressource.
  2. L'utilisateur va s'identifier auprès d'un serveur d'identification et autorisation (dans une mire de connexion) qui lui retourne un jeton d'échange.
  3. Le jeton d'échange est utilisé pour obtenir un jeton d'accès.
  4. Avec ce jeton d'accès, le client accède à cette resource sur l'application possédant la ressource via l'application hébergeant la ressource (resource server).

Les différents termes sont définis dans la terminologie Oauth : https://www.oauth.com/oauth2-servers/definitions/.

Il est donc tout à fait possible d'avoir votre serveur d'autorisation OIDC complètement décorélé de votre serveur hébergeant les ressources. Cela est très intéressant pour mutualiser l'accès à plusieurs serveurs hébergeant des ressources (plusieurs APIs) afin de mutualiser la couche d'identification et permettre des choses sympas comme le SSO. Mais rien ne vous empêche non plus d'avoir les deux (APIs et OIDC) en même temps (c'est ce que nous verrons plus tard dans cet article).

Sous le capot, cela se base sur plusieurs informations techniques :

  • client_id : identifiant de l'application cliente souhaitant accéder à l'application. Chaque application souhaitant s'intégrer à votre système OIDC est normalement déclarée préalablement et en possède un. Il est aussi possible de mettre en place de l'enregistrement dynamique d'application cliente ou de désactiver cette vérification et d'accéder anonymement à vos ressources (d'un point de vue applicatif).
  • client_secret : une sorte de "mot de passe" de votre application cliente associée au client Id.
  • access_token : un jeton donné par le serveur OIDC à votre application cliente pour valider son identité auprès des serveurs fournissant les ressources. Il possède une durée de vie limitée. Ce token est dans le format standard JWT (3 parties séparées par des points, le tout encodé en base64).
  • refresh_token : un jeton permettant d'obtenir un nouvel access_token lorsque le premier obtenu est périmé. Ce mécanisme n'est pas toujours autorisé.
  • id_token : un jeton contenant les informations sur l'utilisateur. La documentation officielle sur le sujet décrit son contenu.

Il est à noter que dans certains cas, il est possible de recevoir deux tokens (id_token et access_token) dans la même réponse du serveur OIDC.

II existe ensuite plusieurs façon de s'identifier auprès de votre serveur OIDC, les plus courantes étant (liste non exhaustive !):

  • Implicit flow : dédié aux applications sans backend (SPA, etc.). L'utilisateur se connecte sur une mire de connexion et le client récupère un jeton d'accès. Une grande partie de la sécurité de ce flow réside dans la présence et l'utilisation du navigateur web. Dans ce contexte, il est recommandé de ne pas permettre de rafraîchir le token. 
  • Authorization code flow : dédié aux applications "serveur" (ou natives avec PKCE me souffle Thomas). L'utilisateur se connecte sur une mire de connexion du serveur OIDC. Le serveur d'authentification appelle une url de callback sur le serveur "client" en fournissant un jeton d'autorisation que celui-ci peut échanger contre un jeton d'accès à l'API. 
  • Resource Owner Password Credentials / Password flow : dédié aux cas où vous avez une relation de confiance très forte avec l'application cliente ou pour des scenarii de migration depuis une authent HTTP Basic. C'est l'application cliente qui présente une mire de connexion et envoi le login/mot de passe de l'utilisateur à votre serveur d'authentification. Cela a beaucoup de désavantages et il faut bien peser le pour et le contre avant de l'utiliser.

Le flow de connexion est le choix le plus important qui sera fait par l'application cliente. Les différents flow font partie de la spécification OAuth 2.0 (la spécification Oauth 1.0 n'en proposant qu'un seul très/trop générique) et ma présentation ici est très succincte. Une présentation plus complète sera peut être l'occasion d'un autre article de blog.

Finalement, il faut noter que chaque demande de connexion est faite pour un périmètre donné (des scopes dans la littérature technique). Le standard OpenIdConnect en définit quelques uns :

  • profile : les infos générales sur l'utilisateur.
  • email : email (vérifié ou non) de l'utilisateur.
  • phone : téléphone (vérifié ou non) de l'utilisateur.
  • openid : obligatoire pour identifier un utilisateur :)

Mise en place technique

La mise en place d'un serveur OIDC est, on peut l'imaginer, assez complexe. Pour faciliter cela, j'utilise la librairie OpenIddict maintenue activement par le français Kévin Chalet. Mon serveur OIDC sera aussi mon serveur hébergeant les APIs : tout est au même endroit.

La mise en place que je propose repose sur ces briques techniques :

  • Asp.Net Core 2.X,
  • Entity Framework Core 2.X

La librairie repose sur le système d'identité / sécurité d'Asp.Net Core et s'intègre très bien avec.

Dans la suite de l'article, nous verrons comment mettre en place le mode d'authentification "Resource Owner Password Credentials".

Configuration du context Entity Framework

La première étape consiste à faire dériver votre contexte du type de base proposé par Asp.Net Core Identity : IdentityDbContext. Dans mon cas, je souhaite que les clefs primaires de mes objets soient des long (et pas des strings comme par défaut) et j'utilise donc son pendant générique.

public abstract class DbContext : 
  IdentityDbContext<User, Role, long>
{
}

Mes entités User et Roles sont des classes dérivant de IdentityUser<long> et IdentityRole<long>.

Il faudra enfin laisser OpenIddict enregistrer ses types dans le DbContext au moment de l'enregistrement de ce dernier en utilisant la méthode UseOpenIddict : 

services.AddDbContext<DbContext>(options =>
{
    options.UseOpenIddict<long>();
});

Configuration du pipeline HTTP Asp.Net Core

La suite se situe au niveau de la méthode ConfigureServices de votre classe de Startup.  On commence par ajouter la couche Identity utilisant EF :

services.AddIdentity<User, Role>()
    .AddEntityFrameworkStores<DbContext>()
    .AddDefaultTokenProviders();

Ensuite, on ajoute la partie authentification en spécifiant que le schéma par défaut est de passer via les token : 

services.AddAuthentication(options =>
{
    options.DefaultScheme = OAuthValidationDefaults.AuthenticationScheme;
}).AddOAuthValidation();

On configure ensuite la partie Identity selon nos goûts et les besoins du projet. Il est aussi important de remplacer le nom des Claims utilisés par défaut pas la partie Identity d'Asp.Net Core (ceux de WS Federation avec des valeurs de ce genre : http://schemas.xmlsoap.org/ws/2005/identity/claims/name) par ceux utilisés par OpenIdConnect (plus simple : name, role, etc.).C'est ce que font les 3 dernières lignes de l'extrait de code :

services.Configure<IdentityOptions>(options =>
{
    // Password settings
    options.Password.RequireDigit = false;
     ....

    options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
    options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
    options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});

Finalement on doit ajouter la partie OpenIddict en elle même. C'est ici que l'on configure ce qui est autorisé ou non d'un point de vue OpenIddict et notamment penser à enregistrer les différents scopes existants.

services.AddOpenIddict()
 .AddCore(options =>
 {
  options.UseEntityFrameworkCore()
   .UseDbContext<TDbContext>()
   .ReplaceDefaultEntities<long>();
 })
 .AddServer(options =>
 {
  // nécessaire pour enregistrer les types OpenIddict
  options.UseMvc();

  // les scopes définis
  options.RegisterScopes(
   OpenIdConnectConstants.Scopes.OpenId,
   OpenIdConnectConstants.Scopes.OfflineAccess);

  // le endpoint de connexion
  options.EnableTokenEndpoint("/connect/token");

  // permettre de se connecter en ressource owned grant..
  options.AllowPasswordFlow();

  // options.AcceptAnonymousClients();
 });

La dernière étape consiste à ajouter le middleware d'authentication. Ajoutez cette ligne au tout début de la méthode Configure de votre Startup : 

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();
}

D'accord, c'est un peu verbeux mais on en a terminé pour le moment :)

Mise en place du contrôleur d'authentification

Il faut maintenant créer un controller permettant d'échanger les informations de connexion de l'utilisateur contre un token. Un sample très complet est proposé sur le GitHub OpenIddictConnect. Globalement, on va utiliser les mécanismes d'Asp.Net Core Identity pour vérifier les informations de connexion de l'utilisateur, configurer un ticket d'authentification et le donner à la méthode SignInAsync (présente sur tous les controllers Asp.Net) pour le laisser faire la connexion de l'utilisateur sous le capot.

Sécuriser un controller

Pour sécuriser l'accès à un controller, il suffit alors de lui apposer les attributs Authorize classiques en spécifiant éventuellement un rôle. Voici un exemple sur une action : 

[Authorize(
 AuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme, 
 Roles = "ADMIN")]
public ActionResult ActionWithAdminAuthorize(){}

Aller plus loin

Cet article n'est qu'un résumé de ma compréhension et il est trèèèsss loin de couvrir le sujet de la sécurisation d'une API dans son ensemble. Je vous propose les lectures suivantes pour aller plus loin :

Happy coding :)

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus