Loupe

Sécuriser une API : connection d'application à application, une API parle à une API.

Dans un précédent article, nous avons vu comment sécuriser une API en suivant le protocole OpenId Connect. La démarche était orientée pour une connexion faite par des utilisateurs humains (ou développeurs). Nous verrons aujourd'hui comment sécuriser vos APIs pour qu'elles soient utilisées par d'autres applications.

Ce scénario permet de répondre à des usages assez fréquents :

  • Un WebJob utilise votre API à heure régulière pour effectuer des traitements en batch.
  • Une AzureFunction utilise votre API en lecture/écriture pour effectuer des opérations.

La notion importante a retenir ici est que vous ne serez pas dans le contexte d'un utilisateur : chaque opération sera faite en tant qu'application et pas en tant qu' utilisateur. Attention donc lorsque vous exposez vos APIs de ne pas avoir cette notion d'identité humaine.

Définition de l'application

Je vais reprendre la base de code mise en place sur le premier article de cette série sécurité en utilisant OpenIddict. 

Pour se connecter en tant qu'application, on va utiliser le grant_type de type "client_credentials". Celui-ci n'est pas lié à OpenId Connect en lui-même mais au protocole Oauth2.0 dont il dérive et permet d'utiliser une application comme identité (plus d'infos sur le site officiel). ATTENTION, ce flow est uniquement à utiliser dans un contexte où vous pouvez garantir la sécurité du couple client_id/client_secret. Dans ce mode, l'utilisation de refresh_token n'est pas possible car il ne fait pas sens : l'application a déjà tout ce qui est nécessaire pour redemander un jeton d'accès si le premier obtenu expire.

Lors de l'enregistrement d'une application permettant de se connecter à l'API sur votre serveur OIDC il faut lui créer un client_id/client_secret et spécifier que celle-ci peut utiliser le grant type susnommé. En reprenant le code de l'article dédié à client_id/client_secret, il faut donc ajouter cette ligne aux permissions : 

 OpenIddictConstants.Permissions.GrantTypes.ClientCredentials

Le contrôleur d'échange de token

Il s'agit de la partie la plus importante : permettre d'échanger une demande de connexion pour un jeton d'accès. Pour cela, on repart de la définition de base d'OpenIddict de l'action dédiée à ce processus et on utilise la méthode IsClientCredentialsGrantType pour n'accepter (dans mon cas) que les demande de connexion avec ce grant_type. Il ne faudrait pas faire ce return si vous souhaitez autoriser d'autres types de connexion.

public async Task<IActionResult> 
  ExchangeAsync([FromBody] OpenIdConnectRequest request)
{
  if (!request.IsClientCredentialsGrantType()){
    return;
  }
}

Une fois que nous sommes sûr d'être sur une demande de connexion provenant d'une application, il va falloir faire plusieurs choses :

  1. Créer une identité (ClaimsIdentity) : on ne peut pas utiliser le SignInManager d'Asp.Net Core vu qu'il n'y a pas d'utilisateur humain dans ce cadre.
  2. Ajouter un claim de type Subject obligatoire pour un bon fonctionnement avec la couche sous-jacente de sécurité Asp.Net Core. J'utilise la valeur de client_id que j'ai dans la requête.
  3. Créer un ticker d'authentification en utilisant l'identité crée.
  4. S'assurer de mettre les bons scopes sur ce ticket. Ici je mets tout ceux passés dans la demande de connexion mais les samples OpenIddict vous donne un très bon exemple pour ne garder que ceux que vous acceptez réellement ici à base de méthode Intersect.
  5. Utiliser le ticket pour demander une connexion via la méthode de base SignIn des contrôleurs. 

Il n'est pas nécessaire de vérifier la véracité de la demande de connexion dans le code du contrôleur car l'infrastructure OpenIddict mise en place s'occupe déjà de cela telle que configurée :

  • Le client_id passé existe bien et le client_secret correspond bien.
  • L'application correspondant à ce client_id a bien le droit d'utiliser ce mode de connexion.
  • Les scopes passés en paramètre existent bien.
  • L'application correspondant à ce client_id a bien le droit d'utiliser ces scopes.

Le code à produire reste alors assez succinct : 

// on créé une identité connecté via OpenId Connect.
var identity = 
  new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(identity);

// On ajoute le claim subject obligatoire
identity.AddClaim(

  new Claim(OpenIdConnectConstants.Claims.Subject, request.ClientId));

// Creation du ticket d'authentification
var ticket = new AuthenticationTicket(
    claimsPrincipal,
    new AuthenticationProperties(),
    OpenIdConnectServerDefaults.AuthenticationScheme);

// Application des scopes
ticket.SetScopes(request.GetScopes()));
return SignIn(
  ticket.Principal, 
  ticket.Properties, 
  ticket.AuthenticationScheme);

Et c'est tout ! L'API demandeuse reçoit un acces_token et peut commencer à l'utiliser pour appeler vos contrôleurs qui sont à protéger de la même manière que si la connexion provenait d'un utilisateur. Un exemple avec l'attribut Authorize était présent dans l'article précédent.

 

Happy coding !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus