Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Se connecter à une API REST protégée par Azure AD avec un site web – partie II

Poster un commentaire

L’authentification d’une appli web à une Web Api n’est pas très différent de celle d’une application native. Nous avons vu qu’il y a deux façons de s’authentifier qui se distinguent tant par les protocoles possibles, que les propriétés de la connexion:

  1. en tant qu’application
    OAuth 2 uniquement
    Scénario: Client Credentials Grant
  2. en tant que délégation d’utilisateur
    OpenId Connect ou OAuth2
    Scénario: Authorization Code Grant

 

Le fait que l’on parle d’une application Web implique toutefois quelques différences d’avec les applications natives puisque plusieurs utilisateurs différents peuvent être connectés simultanément sur la même instance. Il va donc se poser la question de la mise en cache des jetons.

Ce n’est pas un problème pour la connexion comme application qui présente une identité unique à la Web Api, mais nous devrons en tenir compte dans le contexte de la délégation. La délégation d’utilisateur présente par ailleurs l’avantage de pouvoir mettre en place une stratégie de sécurité basée sur les rôles puisque l’on connaît l’utilisateur:

https://amethyste16.wordpress.com/2014/11/11/gerer-lauthentification-et-les-authorisations-avec-les-claims/

Je ne vais pas parler spécialement de ce dernier point dans cet article, mais nous allons voir comment fonctionnent les deux stratégies de connexion de façon concrète. La gestion des rôles n’est pas très difficile à faire ensuite.

Création de l’environnement

L’appli Serveur

Il s’agit de la Web Api de la partie I de cette série d’articles.

Le client web

On a besoin d’un client Web pour dialoguer avec la Web Api. Je l’appellerai ClientWeb. Vous créez un site standard en activant l’authentification « Work or School Accounts » sur le tenant cible. On va donc se connecter avec un compte de l’AD dans ce scénario.

Le site auto-généré met en place des menus de login/logout. Evidemment on peut créer ce code à la main, je ne vais pas le commenter ici, car cela a été fait dans un des articles de cette série.

Au cas où vous vous lancez, vous aurez besoin des packages Nuget suivants:

  • Microsoft.Owin
  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Security.ActiveDirectory
  • Microsoft.Owin.Security.OpenIdConnect
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.IdentityModel.Clients.ActiveDirectory

Une fois terminé vous devriez observez ceci dans votre tenant Azure AD:

 

J’ajoute ensuite cette méthode qui lance un appel à la Web Api:

private async Task<string> CallWebApi(int valeur)
{
   try
   {
      string webApiUrl = "https://VOTREWEBAPI.azurewebsites.net/api/values/{0}";
 
      using (HttpClient client = new HttpClient())
      {
         string url = string.Format(webApiUrl, valeur);
         Uri requestURI = new Uri(url);
         HttpResponseMessage httpResponse = await client.GetAsync(requestURI);
 
         string responseString = await httpResponse.Content.ReadAsStringAsync();
         return responseString;
      }
   }
   catch (Exception ex)
   {
      return ex.Message;
   }
}

Nous la complèterons pour la partie authentification plus tard.

On peut par exemple l’intégrer ainsi:


public async Task<string> Index()
{
   return await CallWebApi(55);
}

Et on a la base de travail qui nous servira dans les deux démos qui suivent.

Si vous lancez maintenant le site Web, vous devriez voir un écran de consentement s’ouvrir:

Ce sont les permission réclamées par défaut par l’appli Web:

Vous pouvez soit cliquer sur ACCEPTER, soit cliquer sur GRANT PERMISSIONS dans le portail pour donner le consentement à tous les utilisateurs de l’appli.

 

 

En fait le site Web a besoin d’autres permissions et en particulier celle d’accéder à la Web Api. La procédure est détaillée ici:

https://amethyste16.wordpress.com/2017/04/05/se-connecter-a-une-api-protegee-par-azure-ad-avec-une-application-native/

Et au final:

 

Lors de la première connexion on verra plutôt ceci apparaître:

 

Comme toujours, je conseille de faire un essai à blanc, c’est à dire sans l’authentification d’activée, simplement pour être certain que les deux applis communiquent. On devrait voir ceci s’afficher:

Une fois testé on remet tout en place.

 

Les choses vont être très simples car on a déjà vu ce scénario avec les applications natives. La différence est que dans un contexte Web, il est plus naturel de se faire rediriger vers une nouvelle page Web pour fournir des credentials.

Il existe deux scénarios typiques et nous allons les faire fonctionner tous les deux:

  1. Autorisation serveur à serveur (Client Credentials Grant)
  2. Autorisation via un code (Authorization Code Grant)

Ces deux scénarios sont ceux-là même démontrés pour une application console dans l’article qui précède, même si je n’avais pas vraiment insisté sur la différence entre eux, je vais le faire un peu plus ici.

Le code va être assez similaire, mais le fait d’être dans une appli web peut avoir tout de même quelques impacts. Un des impacts possibles est que l’appli web soit consultée par un nombre élevé de client. A un instant donné il n’est donc pas impossible que le point de terminaison Azure AD d’authentification soit saturé. Dans du code industriel il faudrait idéalement envisager une politique de réitération de la demande de jeton, gérer les exceptions….

 

Note: dans les exemples qui suit on ne mettra pas en place de politique de réitération pour des raisons de simplification du code.

J’ai déjà parlé des patterns de réitération ici:

https://amethyste16.wordpress.com/2016/09/26/patterns-pour-haute-dispo-et-scalabilite-dune-appli-web-partie-iv/

 

Autorisation serveur à serveur

Le contexte est celui de l’application (application identity). C’est à dire que la ressource ne sait pas qui se logue en réalité, c’est le site qui prête son identité à ses utilisateurs.

Les choses se ramènent à une problématique d’accès à des ressources et OAuth 2 est suffisant.

On commence par les déclarations suivantes:


using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Mvc;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Net.Http.Headers;
using System.Globalization;

  
// id du tenant
static string tenantId = "7dda5ce2-2fb6-4f82-bc27-...";
// url pour obtenir un jeton d'accès
static string tokenUrl = "https://login.microsoftonline.com/{0}/oauth2/token";

// App ID de la web api
private static string webApiAppId = "https://******.onmicrosoft.com/DemoWebApi";

// id du site web
private static string webSiteClientId = "8ce73765-f107-4ec5-9151-....";
// app ID du site web
private static string clientAppId = "https://****.onmicrosoft.com/ClientWeb";
// secret pour le site web
private static string clientAppsecret = "E3cI46aKolLQ0Mn8nxsB58ofFQznnIHr4....=";

private static string authority = string.Format(CultureInfo.InvariantCulture, tokenUrl, tenantId);
private static AuthenticationContext authContext = new AuthenticationContext(authority);

// Deux options possibles:
private static ClientCredential clientCredential = new ClientCredential(webSiteClientId, clientAppsecret);
//private static ClientCredential clientCredential = new ClientCredential(clientAppId, clientAppsecret);

 

On lit des constantes (vous mettrez les vôtres évidemment) qui seront en pratique placée dans le fichier de configuration. Je pense que les commentaire et le nommage sont explicites, je ne reprends pas ici comment les récupérer, j’ai développé ce point dans un précédent article de la série.

On construit ensuite un contexte d’authentification, puis un objet ClientCredential qui sera utilisé lors de la requête vers la Web Api.

La méthode d’appel est modifiée ainsi:


private async Task<string> CallWebApi(int valeur)
{
try
{
   AuthenticationResult result = await authContext.AcquireTokenAsync(webApiAppId, clientCredential);

   string webApiUrl = "https://VOTREWEBAPI.azurewebsites.net/api/values/{0}";

using (HttpClient client = new HttpClient())
{
   string url = string.Format(webApiUrl, valeur);
   Uri requestURI = new Uri(url);

   client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(result.AccessTokenType, result.AccessToken);

   HttpResponseMessage httpResponse = await client.GetAsync(requestURI);
   string responseString = await httpResponse.Content.ReadAsStringAsync();

if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
   authContext.TokenCache.Clear();
}
   return responseString;
}
}
catch (Exception ex)
{
   return ex.Message;
}
}

 

Notez la façon dont est passé le jeton d’accès. La librairie ADAL prend en charge la question du renouvellement du jeton grâce à son cache in-memory fournit par le SDK. Il est toutefois possible de proposer son propre cache.

La propriété result.AccessTokenType retourne « Bearer » puisque l’on est dans un contexte OAuth 2. On pousse le jeton au porteur dans l’entête HTTP qui convient.

Je vous laisse vérifier que tout fonctionne.

 

En principe ce code fonctionne sans problème avec un site Web en accès anonyme puisque seule l’identité de l’application est exploitée. C’est à vous de savoir si cette sécurité vous suffit.

 

Enfin, terminons la dessus. On peut confirmer qu’il s’agit bien d’une authentification serveur à serveur en analysant la signature Fiddler:

POST https://login.microsoftonline.com/7dda5ce2-2fb6-4f82-…/oauth2/token HTTP/1.1
Accept: application/json
x-client-SKU: PCL.Desktop
x-client-Ver: 3.13.9.1126
x-client-CPU: x64
x-client-OS: Microsoft Windows NT 10.0.10586.0
x-ms-PKeyAuth: 1.0
client-request-id: 47fdbb8e-9e4d-4f02-b7cf-1c6ce4c9505f
return-client-request-id: true
Content-Type: application/x-www-form-urlencoded
Host: login.microsoftonline.com
Content-Length: 205
Expect: 100-continue
Connection: Keep-Alive

resource=https://*********.onmicrosoft.com/DemoWebApi&client_id=8ce73765-…&client_secret=E3cI46a..WNYIQlvNM=&grant_type=client_credentials

 

Grant-type vaut client_credentials comme attendu. Vous trouverez quelques détails ici:

https://amethyste16.wordpress.com/2017/04/05/se-connecter-a-une-api-protegee-par-azure-ad-avec-une-application-native/

Autorisation via un code

La contexte est celui de l’utilisateur (delegated user identity).

Le serveur de ressource sait qui se logue en lisant les claims qu’il reçoit. On peut donc envisager une stratégie d’accès aux ressources basées sur les rôles. On dispose de deux possibilités:

  1. Utiliser OpenId Connect
  2. Utiliser OAuth2 Authorization Code Grant

Dans ce contexte il est évident que le site Web ne peut plus accepter d’accès anonymes.

Donc on s’authentifie. Il faut alors résoudre la question de la sauvegarde du jeton de renouvellement. La question se traite par la mise en place d’un cache, côté serveur évidemment. Cette gestion se fera dans la méthode qui gère l’authentification. Nous ne l’avons pas modifiée dans l’exemple qui précède, maintenant ce sera différent.

On a un jeton unique par utilisateur et les utilisateurs seront potentiellement nombreux. ADAL fournit une classe TokenCache qui peut servir de classe de base pour développer son cache maison. C’est ce que nous allons faire car on a besoin de récupérer des infos du cache depuis le contrôleur.

 

Le code

Voyons un peu le code. Comme lors du chapitre qui précède on va se créer un projet MVC appelé ClientWeb2 avec la même méthode CallWebApi que l’on complètera plus loin pour l’authentification.

On déclare tout de suite dans le portail Azure au niveau des permissions que le site a besoin d’accéder à l’Api DemoWebApi. La procédure est évidemment la même que précédemment.

 

Commençons par notre cache.

ADAL fournit nativement un cache (TokenCache). Ce cache est surtout utile pour les applications natives et ne convient pas trop aux applications Web qui ont besoin d’un peu plus de persistance. Il est donc nécessaire de fournir une autre implémentation maison.

J’ai repris un cache trouvé ici:

https://gist.github.com/anonymous/8bb5319fb0b5297f209913ce77bd1f56

Juste un peu modifié le constructeur:


public FileCache(string filePath)
{
   filePath = Path.Combine(@"c:\temp", "cache_" + filePath + ".dat");

   // idem code d'origine
}

On sauvegarde des information encryptées dans un fichier, on aurait pu choisir SQL ou ce que l’on souhaite. Le cache sauvegarde le jeton de renouvellement qui a une durée assez longue normalement.

Si vous avez besoin de quelques infos sur le fonctionnement de TokenCache, je vous renvoie à cet article:

http://www.cloudidentity.com/blog/2014/07/09/the-new-token-cache-in-adal-v2/

L’exemple fournit vous rappellera quelque chose…

La suite se trouve tout d’abord dans la méthode ConfigureAuth automatiquement créée par Visual Studio. C’est ici que se déroule le mécanisme d’authentification.

 

On commence par quelques déclarations qui dans la vraie vie devront se trouver dans le fichier de configuration:


// id du site web
private static string webSiteClientId = "5b50c4cc-cbe2-4559-a846-...";
private static string aadInstance = "https://login.microsoftonline.com/";
// id du tenant Azure AD
private static string tenantId = "7dda5ce2-2fb6-4f82-bc27-...";
private static string postLogoutRedirectUri = "https://localhost:44325/";
private static string authority = aadInstance + tenantId;

// secret pour le site web
private static string clientAppsecret = "WlnW/8i/TKUB....ZkSbSevNYBDc66BC4=";
// App ID de la web api
private static string webApiAppId = "https://fmirouzehotmail.onmicrosoft.com/DemoWebApi";

Je pense que les commentaires et les noms sont clairs. les choses sérieuses démarrent ensuite.

 

Note: il y a des données sensibles comme clientAppSecret. Il est important de les sauvegarder de façon encryptée et sécurisée. Dans Azure la meilleure solution est l’emploi du Key Vault.

Note: On a besoin de l’url de redirection comme paramètre de la méthode qui réclame le jeton d’accès. Dans le scénario en question il n’est pas utilisé, mais la signature de la méthode la réclame, donc….

 

Après avoir installé le package: Microsoft.IdentityModel.Clients.ActiveDirectory

Saisissez le code suivant:

 


public void ConfigureAuth(IAppBuilder app)
{
   app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

   app.UseCookieAuthentication(new CookieAuthenticationOptions());

   // utilise OpenId Connect
   app.UseOpenIdConnectAuthentication(
   new OpenIdConnectAuthenticationOptions
   {
      ClientId = webSiteClientId,
      Authority = authority,
      PostLogoutRedirectUri = postLogoutRedirectUri,
      Notifications = new OpenIdConnectAuthenticationNotifications
      {
         AuthorizationCodeReceived = async (context) =>
        {
           string code = context.Code;
           ClientCredential websiteCredentials = new ClientCredential(webSiteClientId, clientAppsecret);

           string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

           AuthenticationContext authContext = new AuthenticationContext(authority, new FileCache(userObjectID));

           Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
           AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code,
                uri, websiteCredentials, webApiAppId);

           await Task.FromResult(0);
       }
   }
 }
);
}

 

Note: Cet exemple, je l’ai trouvé dans l’excellent (quoique un peu trop technique parfois) bouquin de Vittorio Bertocci:

Modern Authentication with Azure Active Directory for Web Application

 

 

On s’intéresse à la notification AuthorizationCodeReceived. Elle est invoquée si on a reçut un code d’authentification. Si vous vous souvenez du schéma présenté ici:

https://amethyste16.wordpress.com/2017/03/30/proteger-une-application-avec-azure-ad/

Vous savez que le code en question ne permet pas d’accéder à une ressource, juste d’obtenir un access token et un jeton de renouvellement. C’est précisément ce que l’on fait avec la méthode AcquireTokenByAuthorizationCodeAsync.

Une fois le jeton d’accès obtenu, on le pousse dans un cache géré par ADAL afin de pouvoir le récupérer plus tard sans devoir refaire une requête OAuth.

 

AuthenticationContext est une abstraction du tenant Azure AD avec lequel on travaille. On passe donc essentiellement l’id du tenant dans son constructeur ainsi qu’une surcharge du cache par défaut. Cette classe fournit pour l’essentiel des méthodes en AcquireTokenXXXX. Vous devinez de quoi il s’agit.

 

Regardez au moins une fois le contenu de la variable result:

 

Nous avons terminé de ce côté-ci de l’application, rendons nous dans le contrôleur.

Nous utilisons les déclarations suivantes avec les même remarques que juste au dessus:


static string tenantId = "7dda5ce2-2fb6-4f82-bc27-...";
// url pour obtenir un jeton d'accès
static string tokenUrl = "https://login.microsoftonline.com/{0}/oauth2/token";

// App ID de la web api
private static string webApiAppId = "https://fmirouzehotmail.onmicrosoft.com/DemoWebApi";

// id du site web
private static string webSiteClientId = "5b50c4cc-cbe2-4559-a846-...";
// secret pour le site web
private static string clientAppsecret = "WlnW/8i/TKU...QyVVpdcZkSbSevNYBDc66BC4=";

private static string authority = string.Format(CultureInfo.InvariantCulture, tokenUrl, tenantId);
private static AuthenticationContext authContext = new AuthenticationContext(authority);

private static ClientCredential clientCredential = new ClientCredential(webSiteClientId, clientAppsecret);

 

Note: Si vous avez mis en place une stratégie de réitération dans l’exemple qui précède, celle-ci  n’est plus utile ici car si on arrive dans le contrôleur, c’est que l’utilisateur a été authentifié.

Il y a différentes façon d’écrire le code qui suit. Pour ma part j’ai juste réécrit le début de la méthode CallWebApi.


private async Task<string> CallWebApi(int valeur)
{
   try
   {
      // on a besoin de récupérer le contexte d'authentification
      // il se trouve dans le cache et userObjectID et sa clef
      string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
      AuthenticationContext authContext = new AuthenticationContext(authority, new FileCache(userObjectID));
      UserIdentifier userIdentifier = new UserIdentifier(userObjectID, UserIdentifierType.UniqueId);
      ClientCredential credentials = new ClientCredential(webSiteClientId, clientAppsecret);

      // s'il y a un jeton de renouvellement, alors obtient un access token (result.AccessToken) grâce au refresh token
      AuthenticationResult result = await authContext.AcquireTokenSilentAsync(webApiAppId, credentials, userIdentifier);

 
      string webApiUrl = "https://demowebapi20170604022813.azurewebsites.net/api/values/{0}";

      using (HttpClient client = new HttpClient())
      {
         string url = string.Format(webApiUrl, valeur);
         Uri requestURI = new Uri(url);

         client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(result.AccessTokenType, result.AccessToken);

         HttpResponseMessage httpResponse = await client.GetAsync(requestURI);
         string responseString = await httpResponse.Content.ReadAsStringAsync();

         if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
         {
            authContext.TokenCache.Clear();
         }
         return responseString;
      }
   }
   catch (Exception ex)
   {
      return ex.Message;
   }
}

Le code en question est relativement simple je pense.

Un mot sur la variante AcquireTokenSilentAsync. Elle fonctionne comme les autres, mais sans afficher le cérémonial du formulaire d’identification. Le jeton est retrouvé depuis le cache que nous avons mis en place dans Startup.cs.

 

Lancez le site et après une authentification suivie éventuellement d’un formulaire d’acceptation, vous devriez voir quelque chose de ce genre:

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s