Exercice : incorporer la bibliothèque d’authentification Microsoft dans les applications web ASP.NET MVC
Dans cet exercice, vous allez étendre l’application de l’exercice précédent pour prendre en charge l’authentification avec l’ID Microsoft Entra. Cette opération est obligatoire pour obtenir le jeton d’accès OAuth nécessaire pour appeler l’API Microsoft Graph. Dans cette étape, vous allez intégrer l’intergiciel OWIN et la bibliothèque d’authentification Microsoft dans l’application.
Cliquez avec le bouton droit sur le projet graph-tutorial dans l’Explorateur de solutions, puis sélectionnez Ajouter un > nouvel élément...
Sélectionnez Fichier de configuration web, nommez le fichier PrivateSettings.config puis sélectionnez Ajouter.
Remplacez l’ensemble du contenu par le code suivant :
<appSettings>
<add key="ida:AppID" value="YOUR APP ID" />
<add key="ida:AppSecret" value="YOUR APP PASSWORD" />
<add key="ida:RedirectUri" value="https://localhost:PORT/" />
<add key="ida:AppScopes" value="User.Read Calendars.Read" />
</appSettings>
Remplacez par YOUR_APP_ID_HERE
l’ID d’application du Centre d’administration Microsoft Entra, puis remplacez par YOUR_APP_PASSWORD_HERE
la clé secrète client que vous avez générée. Si votre secret client contient des signes esperluette (&
), n’oubliez pas de les remplacer par des &
dans PrivateSettings.config
. Assurez-vous également de modifier la valeur PORT
en ida:RedirectUri
afin qu’elle corresponde à l’URL de votre application.
Importante
Si vous utilisez le contrôle de code source comme git, il est recommandé d’exclure le fichier PrivateSettings.config du contrôle de source pour éviter de divulguer votre ID d’application et votre mot de passe par inadvertance.
Mettez à jour Web.config pour charger ce nouveau fichier. Remplacez <appSettings>
(ligne 7) par ce qui suit.
<appSettings file="PrivateSettings.config">
Implémentation de la connexion
Commencez par initialiser l’intergiciel OWIN pour utiliser l’authentification Microsoft Entra pour l’application.
Cliquez avec le bouton droit sur le dossier App_Start dans l’Explorateur de solutions, puis sélectionnez Ajouter une > classe.... Nommez le fichier Startup.Auth.cs , puis sélectionnez Ajouter. Remplacez tout le contenu par le code suivant.
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System.Configuration;
using System.Threading.Tasks;
using System.Web;
namespace graph_tutorial
{
public partial class Startup
{
// Load configuration settings from PrivateSettings.config
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = false
// In a real multi-tenant app, you would add logic to determine whether the
// issuer was from an authorized tenant
//ValidateIssuer = true,
//IssuerValidator = (issuer, token, tvp) =>
//{
// if (MyCustomTenantValidation(issuer))
// {
// return issuer;
// }
// else
// {
// throw new SecurityTokenInvalidIssuerException("Invalid issuer");
// }
//}
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
private static Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
string redirect = $"/Home/Error?message={notification.Exception.Message}";
if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
{
redirect += $"&debug={notification.ProtocolMessage.ErrorDescription}";
}
notification.Response.Redirect(redirect);
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
string message;
string debug;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
message = "Access token retrieved.";
debug = result.AccessToken;
}
catch (MsalException ex)
{
message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
debug = ex.Message;
}
var queryString = $"message={message}&debug={debug}";
if (queryString.Length > 2048)
{
queryString = queryString.Substring(0, 2040) + "...";
}
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?{queryString}");
}
}
}
Remarque
Ce code configure l’intergiciel OWIN avec les valeurs de PrivateSettings.config et définit deux méthodes de rappel, OnAuthenticationFailedAsync
et OnAuthorizationCodeReceivedAsync
. Ces méthodes de rappel sont appelées lorsque le processus de connexion renvoie à partir d’Azure.
Mettez à jour le fichier Startup.cs pour appeler la méthode ConfigureAuth
. Remplacez tout le contenu de Startup.cs par le code suivant.
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(graph_tutorial.Startup))]
namespace graph_tutorial
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
Ajoutez une action Error
à la classe HomeController
pour transformer le message
et debug
les paramètres de requête en un objet Alert
. Ouvrez Controllers/HomeController.cs et ajoutez la fonction suivante.
public ActionResult Error(string message, string debug)
{
Flash(message, debug);
return RedirectToAction("Index");
}
Ajouter un contrôleur pour gérer la connexion. Cliquez avec le bouton droit sur le dossier Contrôleurs dans l’Explorateur de solutions, puis sélectionnez Ajouter un > contrôleur.... Choisissez Contrôleur MVC 5 - Vide , puis sélectionnez Ajouter. Nommez le contrôleur AccountController puis sélectionnez Ajouter. Remplacez tout le contenu du fichier par le code suivant.
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
namespace graph_tutorial.Controllers
{
public class AccountController : Controller
{
public void SignIn()
{
if (!Request.IsAuthenticated)
{
// Signal OWIN to send an authorization request to Azure
Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
public ActionResult SignOut()
{
if (Request.IsAuthenticated)
{
Request.GetOwinContext().Authentication.SignOut(
CookieAuthenticationDefaults.AuthenticationType);
}
return RedirectToAction("Index", "Home");
}
}
}
Cela définit une action SignIn
et une action SignOut
. L’action SignIn
vérifie si la demande est déjà authentifiée. Si ce n’est pas le cas, il appelle l’intergiciel OWIN pour authentifier l’utilisateur. L’action SignOut
appelle l’intergiciel OWIN pour se déconnecter.
Enregistrez vos modifications et démarrez le projet. Sélectionnez le bouton Connexion et vous devez être redirigé vers https://login.microsoftonline.com
. Connectez-vous avec votre compte Microsoft et acceptez les autorisations demandées. Le navigateur vous redirige vers l’application, affichant le jeton.
Obtenir les détails de l’utilisateur
Une fois que l’utilisateur a ouvert une session, vous pouvez obtenir ses informations à partir de Microsoft Graph.
Cliquez avec le bouton droit sur le dossier Models dans l’Explorateur de solutions, puis sélectionnez Ajouter une > classe.... Nommez la classe CachedUser et sélectionnez Ajouter. Remplacez tout le contenu de CachedUser.cs par le code suivant.
namespace graph_tutorial.Models
{
// Simple class to serialize user details
public class CachedUser
{
public string DisplayName { get; set; }
public string Email { get; set; }
public string Avatar { get; set; }
}
}
Cliquez avec le bouton droit sur le dossier graph-tutorial dans l’Explorateur de solutions, puis sélectionnez Ajouter un > nouveau dossier. Nommez le dossier Helpers.
Cliquez avec le bouton droit sur ce nouveau dossier, puis sélectionnez Ajouter une > classe.... Nommez le fichier GraphHelper.cs , puis sélectionnez Ajouter. Remplacez le contenu du fichier par le code suivant.
using graph_tutorial.Models;
using Microsoft.Graph;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace graph_tutorial.Helpers
{
public static class GraphHelper
{
public static async Task<CachedUser> GetUserDetailsAsync(string accessToken)
{
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}));
var user = await graphClient.Me.Request()
.Select(u => new {
u.DisplayName,
u.Mail,
u.UserPrincipalName
})
.GetAsync();
return new CachedUser
{
Avatar = string.Empty,
DisplayName = user.DisplayName,
Email = string.IsNullOrEmpty(user.Mail) ?
user.UserPrincipalName : user.Mail
};
}
}
}
Cette opération implémente la fonction GetUserDetailsAsync
, qui utilise le kit de développement logiciel Microsoft Graph pour appeler le point de terminaison /me
et renvoyer le résultat.
Mettez à jour la méthode OnAuthorizationCodeReceivedAsync
dans App_Start/Startup.Auth.cs pour appeler cette fonction. Ajoutez l’instruction using
suivante en haut du fichier.
using graph_tutorial.Helpers;
Remplacez le bloc try
existant dans OnAuthorizationCodeReceivedAsync
par le code suivant.
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
message = "User info retrieved.";
debug = $"User: {userDetails.DisplayName}, Email: {userDetails.Email}";
}
Enregistrer vos modifications et démarrer l’application. Après vous être connecté, vous devriez voir le nom et l’adresse de messagerie de l’utilisateur au lieu du jeton d’accès.
Stockage des jetons
Maintenant que vous pouvez obtenir des jetons, nous vous conseillons d’implémenter un moyen de les stocker dans l’application. Comme il s’agit d’un exemple d’application, vous utiliserez la session pour stocker les jetons. Une application réelle utilise une solution de stockage sécurisé plus fiable, comme une base de données. Dans cette section, vous allez :
- Implémenter une classe stockage de jetons pour sérialiser et stocker le cache de jetons MSAL et les détails de l’utilisateur dans la session utilisateur.
- Mettre à jour le code d’authentification pour utiliser la classe stockage de jetons.
- Mettre à jour la classe contrôleur de base pour exposer les détails stockés de l’utilisateur, à tous les affichages dans l’application.
Cliquez avec le bouton droit sur le dossier graph-tutorial dans l’Explorateur de solutions, puis sélectionnez Ajouter un > nouveau dossier. Nommez le dossier TokenStorage.
Cliquez avec le bouton droit sur ce nouveau dossier, puis sélectionnez Ajouter une > classe.... Nommez le fichier SessionTokenStore.cs , puis sélectionnez Ajouter. Remplacez le contenu du fichier par le code suivant.
using graph_tutorial.Models;
using Microsoft.Identity.Client;
using Newtonsoft.Json;
using System.Security.Claims;
using System.Threading;
using System.Web;
namespace graph_tutorial.TokenStorage
{
public class SessionTokenStore
{
private static readonly ReaderWriterLockSlim sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private HttpContext httpContext = null;
private string tokenCacheKey = string.Empty;
private string userCacheKey = string.Empty;
public SessionTokenStore(ITokenCache tokenCache, HttpContext context, ClaimsPrincipal user)
{
httpContext = context;
if (tokenCache != null)
{
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
var userId = GetUsersUniqueId(user);
tokenCacheKey = $"{userId}_TokenCache";
userCacheKey = $"{userId}_UserCache";
}
public bool HasData()
{
return (httpContext.Session[tokenCacheKey] != null &&
((byte[])httpContext.Session[tokenCacheKey]).Length > 0);
}
public void Clear()
{
sessionLock.EnterWriteLock();
try
{
httpContext.Session.Remove(tokenCacheKey);
}
finally
{
sessionLock.ExitWriteLock();
}
}
private void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
sessionLock.EnterReadLock();
try
{
// Load the cache from the session
args.TokenCache.DeserializeMsalV3((byte[])httpContext.Session[tokenCacheKey]);
}
finally
{
sessionLock.ExitReadLock();
}
}
private void AfterAccessNotification(TokenCacheNotificationArgs args)
{
if (args.HasStateChanged)
{
sessionLock.EnterWriteLock();
try
{
// Store the serialized cache in the session
httpContext.Session[tokenCacheKey] = args.TokenCache.SerializeMsalV3();
}
finally
{
sessionLock.ExitWriteLock();
}
}
}
public void SaveUserDetails(CachedUser user)
{
sessionLock.EnterWriteLock();
httpContext.Session[userCacheKey] = JsonConvert.SerializeObject(user);
sessionLock.ExitWriteLock();
}
public CachedUser GetUserDetails()
{
sessionLock.EnterReadLock();
var cachedUser = JsonConvert.DeserializeObject<CachedUser>((string)httpContext.Session[userCacheKey]);
sessionLock.ExitReadLock();
return cachedUser;
}
public string GetUsersUniqueId(ClaimsPrincipal user)
{
// Combine the user's object ID with their tenant ID
if (user != null)
{
var userObjectId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value ??
user.FindFirst("oid").Value;
var userTenantId = user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value ??
user.FindFirst("tid").Value;
if (!string.IsNullOrEmpty(userObjectId) && !string.IsNullOrEmpty(userTenantId))
{
return $"{userObjectId}.{userTenantId}";
}
}
return null;
}
}
}
Ajouter l’instruction using
suivante en haut du fichier App_Start/Startup.Auth.cs.
using graph_tutorial.TokenStorage;
using System.Security.Claims;
Remplacer la fonction OnAuthorizationCodeReceivedAsync
existante par ce qui suit.
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
notification.HandleCodeRedemption();
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var signedInUser = new ClaimsPrincipal(notification.AuthenticationTicket.Identity);
var tokenStore = new SessionTokenStore(idClient.UserTokenCache, HttpContext.Current, signedInUser);
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
tokenStore.SaveUserDetails(userDetails);
notification.HandleCodeRedemption(null, result.IdToken);
}
catch (MsalException ex)
{
string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
}
catch (Microsoft.Graph.ServiceException ex)
{
string message = "GetUserDetailsAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
}
}
Remarque
Les modifications apportées à cette nouvelle version de OnAuthorizationCodeReceivedAsync
apportent ces changements :
- Le code encapsule désormais le cache de jetons utilisateur par défaut de
ConfidentialClientApplication
avec la classeSessionTokenStore
. La bibliothèque MSAL gère la logique de stockage des jetons et les actualise en cas de besoin. - Le code transmet désormais les informations utilisateur obtenues à partir de Microsoft Graph à l’objet
SessionTokenStore
à stocker dans la session. - En cas de réussite, le code ne redirige plus, il renvoie simplement. Cela permet à l’intergiciel OWIN de terminer le processus d’authentification.
Mettre à jour l’action SignOut
pour effacer le stockage de jetons avant de vous déconnecter. Ajouter l’instruction using
suivante en haut de Controllers/AccountController.cs.
using graph_tutorial.TokenStorage;
Remplacez la fonction SignOut
existante par ce qui suit.
public ActionResult SignOut()
{
if (Request.IsAuthenticated)
{
var tokenStore = new SessionTokenStore(null,
System.Web.HttpContext.Current, ClaimsPrincipal.Current);
tokenStore.Clear();
Request.GetOwinContext().Authentication.SignOut(
CookieAuthenticationDefaults.AuthenticationType);
}
return RedirectToAction("Index", "Home");
}
Ouvrez Controllers/BaseController.cs et ajoutez les instructions using
suivantes en haut du fichier.
using graph_tutorial.TokenStorage;
using System.Security.Claims;
using System.Web;
using Microsoft.Owin.Security.Cookies;
Ajoutez la fonction suivante.
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (Request.IsAuthenticated)
{
// Get the user's token cache
var tokenStore = new SessionTokenStore(null,
System.Web.HttpContext.Current, ClaimsPrincipal.Current);
if (tokenStore.HasData())
{
// Add the user to the view bag
ViewBag.User = tokenStore.GetUserDetails();
}
else
{
// The session has lost data. This happens often
// when debugging. Log out so the user can log back in
Request.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
filterContext.Result = RedirectToAction("Index", "Home");
}
}
base.OnActionExecuting(filterContext);
}
Démarrez le serveur et suivez le processus de connexion. Vous vous retrouvez sur la page d’accueil, mais l’interface utilisateur a changé et indique que vous êtes connecté.
Sélectionnez l’avatar de l’utilisateur dans le coin supérieur droit pour accéder au lien Se déconnecter. La sélection Se déconnecter réinitialise la session et vous ramène à la page d’accueil.
Actualisation des jetons
À ce stade, votre application possède un jeton d’accès, qui est envoyé dans l’en-tête Authorization
des appels API. Il s’agit du jeton qui permet à l’application d’accéder à Microsoft Graph pour le compte de l’utilisateur.
Cependant, ce jeton est de courte durée. Le jeton arrive à expiration une heure après son émission. C’est là que le jeton d’actualisation devient utile. Le jeton d’actualisation permet à l’application de demander un nouveau jeton d’accès sans obliger l’utilisateur à se reconnecter.
Comme l’application utilise la bibliothèque MSAL et sérialise l’objet TokenCache
, vous n’avez pas besoin d’implémenter de logique d’actualisation de jeton. La méthode ConfidentialClientApplication.AcquireTokenSilentAsync
fait la logique pour vous. Elle examine tout d’abord le jeton mis en cache et, s’il n’a pas expiré, elle renvoie celui-ci. Si il est expiré, elle utilise le jeton d’actualisation mis en cache pour en obtenir un nouveau. Vous utiliserez cette méthode dans un exercice ultérieur.
Résumé
Dans cet exercice, vous avez étendu l’application de l’exercice précédent pour prendre en charge l’authentification avec l’ID Microsoft Entra. Cette opération est obligatoire pour obtenir le jeton d’accès OAuth nécessaire pour appeler l’API Microsoft Graph. Dans cette étape, vous allez intégrer les intergiciels OWIN et la Bibliothèque d’authentification Microsoft dans l’application.