Authentification et autorisation dans ASP.NET Core SignalR

Cet article explique comment authentifier et autoriser les utilisateurs dans ASP.NET Core applications avec SignalR.

Authentifier les utilisateurs qui se connectent à un hub SignalR

SignalRpeut être utilisé avec l’authentification ASP.NET Core pour associer un utilisateur à chaque connexion. Dans un hub, les données d’authentification sont accessibles à partir de la propriété HubConnectionContext.User. L’authentification permet au hub d’appeler des méthodes sur toutes les connexions associées à un utilisateur. Pour plus d’informations, consultez Gérer les utilisateurs et les groupes dans SignalR. Plusieurs connexions peuvent être associées à un seul utilisateur.

Le code suivant est un exemple qui utilise SignalR et l’authentification ASP.NET Core :

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chat");

app.Run();

Note

Si un jeton expire pendant la durée de vie d’une connexion, par défaut, la connexion continue de fonctionner. Les connexions LongPolling et ServerSentEvents échouent lors des demandes suivantes si elles n’envoient pas de nouveaux jetons d’accès. Pour que les connexions se ferment lorsque le jeton d’authentification expire, définissez l’option CloseOnAuthenticationExpiration .

Modification de l’utilisateur et du rôle pendant la durée de vie de la connexion

SignalR capture l’utilisateur authentifié lorsqu’une connexion est établie et la met en cache pendant toute la durée de vie de la connexion. Le principal mis en cache est exposé aux méthodes hub via la Context.User propriété (HubCallerContext.User) et est utilisé pour autoriser les appels de méthode hub. SignalR ne revalide pas automatiquement l’utilisateur pendant la durée de vie de la connexion, quel que soit le schéma d’authentification. Ce comportement s’applique à tous les schémas d’authentification, y compris l'authentification cookie et l'authentification par jeton du porteur.

Les modifications apportées à l’identité, aux rôles ou aux revendications d’un utilisateur qui se produisent une fois la connexion établie ne sont pas reflétées sur une connexion existante. Ce comportement s’applique même lorsque le transport sous-jacent effectue de nouvelles requêtes HTTP, comme les transports LongPolling et ServerSentEvents. Bien que le middleware d’authentification ASP.NET Core réauthentifie chacune de ces requêtes HTTP, SignalR continue d’utiliser le principal mis en cache lors de l’établissement de la connexion.

Examinez le cas suivant :

  • Une application utilise une authentification cookie et le transport LongPolling. Le même comportement s’applique à l’authentification par jeton du porteur et au transport ServerSentEvents.
  • Un utilisateur est connecté avec le Editor rôle et dispose d’une connexion ouverte SignalR .
  • L’application supprime le Editor rôle de cet utilisateur.

Lors de la prochaine requête long-polling, le middleware d’authentification pourrait authentifier l’utilisateur sans le rôle Editor. Par exemple, cela peut se produire si des rôles sont chargés à partir d’un magasin de données ou si cookie est rafraîchi. Toutefois, le hub continue d’autoriser les invocations de méthode de hub [Authorize(Roles = "Editor")] par l’utilisateur, car Context.User (HubCallerContext.User) détient toujours le principal mis en cache avant la suppression du rôle. L’utilisateur peut continuer à appeler uniquement les méthodes de hub Editor jusqu’à ce que la connexion soit fermée.

Pour appliquer l’autorisation mise à jour sur une connexion active, effectuez l’une des approches suivantes :

  • Fermez les connexions affectées afin que les clients se reconnectent et se réauthentifier. Pour l’authentification par jeton du porteur, l’option CloseOnAuthenticationExpiration ferme les connexions lorsque le jeton d’authentification expire.
  • Effectuez des vérifications d’autorisation dans les méthodes du hub à partir des données actuelles, telles que les rôles actuels de l’utilisateur ou ses déclarations stockées dans un magasin de données, au lieu de vous appuyer uniquement sur le principal mis en cache.

Dans une application basée sur un navigateur, l’authentification cookie permet aux informations d’identification utilisateur existantes de circuler automatiquement vers les connexions SignalR. Lorsque le client de navigateur est utilisé, aucune configuration supplémentaire n’est nécessaire. Si l’utilisateur est connecté à une application, la SignalR connexion hérite automatiquement de cette authentification.

Les cookies sont un moyen spécifique au navigateur d’envoyer des jetons d’accès, mais les clients non-rowser peuvent les envoyer. Lorsque le client .NET est utilisé, la Cookies propriété peut être configurée dans l’appel .WithUrl pour fournir un cookie. Toutefois, l’utilisation de l’authentification cookie à partir du client .NET nécessite que l’application fournisse une API pour échanger des données d’authentification pour un cookie.

Important

Depuis ASP.NET Core 10, les points de terminaison d'API connus ne redirigent plus vers les pages de connexion lors de l'utilisation de cookie pour l'authentification. Au lieu de cela, ils retournent les codes d’état 401/403. Pour plus d’informations, consultez le comportement d’authentification du point de terminaison d’API dans ASP.NET Core.

Authentification des jetons du porteur

Le client peut fournir un jeton d’accès au lieu d’utiliser un cookie. Le serveur valide le jeton et l’utilise pour identifier l’utilisateur. Pour les transports qui effectuent plusieurs requêtes HTTP (par exemple, LongPolling et ServerSentEvents), l’authentification s’exécute sur chaque requête, mais SignalR met en cache le principal résultant pour la durée de vie de la connexion. Comme avec cookie l’authentification, SignalR ne revalide pas automatiquement l’utilisateur pendant la durée de vie de la connexion afin de vérifier la révocation des jetons ou des modifications apportées aux rôles ou revendications de l’utilisateur. Pour plus d’informations, consultez Modifications de rôle et d’utilisateur pendant la durée de vie de la connexion.

Dans le client JavaScript, le jeton peut être fourni à l’aide de l’option accessTokenFactory .

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

Dans le client .NET, il existe une propriété AccessTokenProvider similaire qui peut être utilisée pour configurer le jeton :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Note

La fonction de jeton d’accès est appelée avant chaque requête HTTP effectuée par SignalR. Si le jeton doit être renouvelé pour conserver la connexion active, effectuez le renouvellement à partir de cette fonction et retournez le jeton mis à jour. Le jeton peut avoir besoin d’être renouvelé afin qu’il n’expire pas pendant la connexion.

Dans les API web standard, les jetons d'accès sont envoyés dans un en-tête HTTP. Toutefois, SignalR ne peut pas définir ces en-têtes dans les navigateurs lorsque certains transports sont utilisés. Lorsque WebSockets et les événements envoyés par le serveur sont utilisés, le jeton est transmis sous forme de paramètre de chaîne de requête.

Authentification JWT intégrée

Sur le serveur, l’authentification par jeton Bearer est configurée à l’aide du middleware Bearer JSON Web Token (JWT) :

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
{
    // Identity made Cookie authentication the default.
    // However, we want JWT Bearer Auth to be the default.
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
  {
      // Configure the Authority to the expected value for
      // the authentication provider. This ensures the token
      // is appropriately validated.
      options.Authority = "Authority URL"; // TODO: Update URL

      // We have to hook the OnMessageReceived event in order to
      // allow the JWT authentication handler to read the access
      // token from the query string when a WebSocket or 
      // Server-Sent Events request comes in.

      // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
      // due to a limitation in Browser APIs. We restrict it to only calls to the
      // SignalR hub in this code.
      // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
      // for more information about security considerations when using
      // the query string to transmit the access token.
      options.Events = new JwtBearerEvents
      {
          OnMessageReceived = context =>
          {
              var accessToken = context.Request.Query["access_token"];

              // If the request is for our hub...
              var path = context.HttpContext.Request.Path;
              if (!string.IsNullOrEmpty(accessToken) &&
                  (path.StartsWithSegments("/hubs/chat")))
              {
                  // Read the token out of the query string
                  context.Token = accessToken;
              }
              return Task.CompletedTask;
          }
      };
  });

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token 
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages 
// intended for a different user!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the 
// EmailBasedUserIdProvider, but do not use both. 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Note

La chaîne de requête est utilisée sur les navigateurs lors de la connexion avec des événements WebSocket et Server-Sent en raison des limitations de l’API du navigateur. Lorsque vous utilisez HTTPS, la connexion TLS sécurise les valeurs de chaîne de requête. Toutefois, de nombreux serveurs consignent les valeurs de chaîne de requête. Pour plus d'informations, consultez Considérations sur la sécurité dans ASP.NET Core SignalR. SignalRutilise des en-têtes pour transmettre des jetons dans des environnements qui les prennent en charge, tels que le .NET et les clients Java.

Identity Authentification JWT du serveur

Lorsque vous utilisez Duende IdentityServer, ajoutez un PostConfigureOptions<TOptions> service au projet :

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Enregistrez le service après avoir ajouté les services d’authentification (avec la méthode AddAuthentication) et le gestionnaire d’authentification pour le serveur Identity (avec la méthode AddIdentityServerJwt) :

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
        ConfigureJwtBearerOptions>());

builder.Services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Cookies par rapport aux jetons du porteur

Les cookies sont spécifiques aux navigateurs. L’envoi de ces jetons à partir d’autres types de clients ajoute de la complexité par rapport à l’envoi de jetons du porteur. L’authentification Cookie n’est pas recommandée, sauf si l’application doit uniquement authentifier les utilisateurs à partir du client de navigateur. L’authentification par jeton du porteur est l’approche recommandée lors de l’utilisation de clients autres que le client de navigateur.

Authentification Windows

Si l’authentification Windows est configurée dans l’application, SignalR elle peut utiliser cette identité pour sécuriser les hubs. Toutefois, pour envoyer des messages à des utilisateurs individuels, ajoutez un fournisseur d’ID utilisateur personnalisé. Le système d'authentification Windows ne fournit pas la revendication « Identifiant de nom ». SignalR utilise la revendication pour déterminer le nom d’utilisateur.

Ajoutez une nouvelle classe qui implémente IUserIdProvider et récupère l’une des revendications de l’utilisateur à utiliser comme identificateur. Par exemple, pour utiliser la revendication « Name » (qui est le nom d’utilisateur Windows au format [Domain]/[Username]), créez la classe suivante :

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Au lieu de ClaimTypes.Name, utilisez n’importe quelle valeur du User, comme l’identificateur SID Windows, etc.

Note

La valeur spécifiée doit être unique parmi tous les utilisateurs du système. Sinon, un message destiné à un utilisateur peut finir par atteindre un autre utilisateur.

Inscrivez ce composant dans le fichier Program.cs :

using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.SignalR;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();

services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

var app = builder.Build();

// Code removed for brevity.

Dans le client .NET, Authentification Windows doit être activé en définissant la UseDefaultCredentials propriété :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

L'authentification Windows est prise en charge dans Microsoft Edge, mais pas dans tous les navigateurs. Par exemple, dans Chrome et Safari, la tentative d’utilisation de Authentification Windows et WebSockets échoue. Lorsque l’authentification Windows échoue, le client tente de se rabattre sur d’autres transports, susceptibles de fonctionner.

Utiliser des revendications pour personnaliser la gestion des identités

Une application qui authentifie les utilisateurs peut dériver SignalR des ID d’utilisateur à partir de déclarations utilisateur. Pour spécifier la façon dont SignalR crée les ID utilisateur, implémentez IUserIdProvider et inscrivez l’implémentation.

L’exemple de code montre comment utiliser des revendications pour sélectionner l’adresse e-mail de l’utilisateur comme propriété d’identification.

Note

La valeur spécifiée doit être unique parmi tous les utilisateurs du système. Sinon, un message destiné à un utilisateur peut finir par atteindre un autre utilisateur.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value!;
    }
}

L’inscription du compte ajoute une revendication avec type ClaimsTypes.Email à la base de données d’identité ASP.NET.

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        var result = await _userManager.CreateAsync(user, Input.Password);

        // Add the email claim and value for this user.
        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

        // Remaining code removed for brevity.

Inscrivez ce composant dans le fichier Program.cs :

builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autoriser les utilisateurs à accéder aux hubs et aux méthodes des hubs

Par défaut, un utilisateur non authentifié peut appeler toutes les méthodes dans un hub. Pour exiger l'authentification, appliquez l'attribut AuthorizeAttribute au concentrateur :

[Authorize]
public class ChatHub: Hub
{
}

Les arguments du constructeur et les propriétés de l'attribut[Authorize] peuvent être utilisés pour restreindre l'accès aux seuls utilisateurs correspondant à des stratégies d'autorisation spécifiques. Par exemple, avec la stratégie d’autorisation personnalisée appelée MyAuthorizationPolicy, seuls les utilisateurs correspondant à cette stratégie peuvent accéder au hub à l’aide du code suivant :

[Authorize("MyAuthorizationPolicy")]
public class ChatPolicyHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("ReceiveSystemMessage", 
                                    $"{Context.UserIdentifier} joined.");
        await base.OnConnectedAsync();
    }
    // Code removed for brevity.

L’attribut [Authorize] peut être appliqué à des méthodes hub individuelles. Si l’utilisateur actuel ne correspond pas aux stratégies appliquées à la méthode, une erreur est retournée à l’appelant :

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... Send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... Ban a user from the chat room (something only Administrators can do) ...
    }
}

Utiliser des gestionnaires d’autorisation pour personnaliser l’autorisation de méthode hub

SignalR fournit une ressource personnalisée aux gestionnaires d’autorisation lorsqu’une méthode hub nécessite une autorisation. La ressource est une instance de HubInvocationContext. Le HubInvocationContext inclut le HubCallerContext, le nom de la méthode hub appelée et les arguments de la méthode hub.

Prenons l’exemple d’une salle de conversation qui permet à plusieurs organisations de se connecter via Microsoft Entra ID. Toute personne disposant d’un compte Microsoft peut se connecter à la conversation, mais seuls les membres de l’organisation propriétaire devraient pouvoir interdire les utilisateurs ou afficher les historiques de conversation des utilisateurs. En outre, il peut être nécessaire de restreindre certaines fonctionnalités d’utilisateurs spécifiques. Dans ce scénario, notez comment le DomainRestrictedRequirement sert de IAuthorizationRequirement personnalisé. Étant donné que le HubInvocationContext paramètre de ressource est transmis, la logique interne peut inspecter le contexte dans lequel le hub est appelé et prendre des décisions sur l’autorisation de l’utilisateur d’exécuter des méthodes de hub individuelles :

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace SignalRAuthenticationSample;

public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (context.User.Identity != null &&
          !string.IsNullOrEmpty(context.User.Identity.Name) && 
          IsUserAllowedToDoThis(resource.HubMethodName,
                               context.User.Identity.Name) &&
          context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
                context.Succeed(requirement);
            
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") &&
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

Dans le fichier Program.cs , ajoutez la nouvelle stratégie, en fournissant l’exigence personnalisée DomainRestrictedRequirement en tant que paramètre pour créer la DomainRestricted stratégie :

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var services = builder.Services;

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
services.AddDatabaseDeveloperPageExceptionFilter();

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthorization(options =>
   {
       options.AddPolicy("DomainRestricted", policy =>
       {
           policy.Requirements.Add(new DomainRestrictedRequirement());
       });
   });

services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Dans l’exemple précédent, la classe DomainRestrictedRequirement est à la fois un IAuthorizationRequirement et son propre AuthorizationHandler pour cette exigence. Il est acceptable de diviser ces deux composants en classes distinctes pour séparer les préoccupations. L’approche de cet exemple offre l’avantage de ne pas avoir à injecter AuthorizationHandler au démarrage, car l’exigence et le gestionnaire ne font qu’un.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Authentifier les utilisateurs qui se connectent à un hub SignalR

SignalRpeut être utilisé avec l’authentification ASP.NET Core pour associer un utilisateur à chaque connexion. Dans un hub, les données d’authentification sont accessibles à partir de la propriété HubConnectionContext.User. L’authentification permet au hub d’appeler des méthodes sur toutes les connexions associées à un utilisateur. Pour plus d’informations, consultez Gérer les utilisateurs et les groupes dans SignalR. Plusieurs connexions peuvent être associées à un seul utilisateur.

Voici un exemple de Startup.Configure qui utilise SignalR et l’authentification ASP.NET Core :

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Note

Si un jeton expire pendant la durée de vie d’une connexion, la connexion continue de fonctionner. Les connexions LongPolling et ServerSentEvents échouent lors des demandes suivantes si elles n’envoient pas de nouveaux jetons d’accès.

Dans une application basée sur un navigateur, l’authentification cookie permet à vos informations d’identification utilisateur existantes de circuler automatiquement vers les connexions SignalR. Lors de l’utilisation du client de navigateur, aucune configuration supplémentaire n’est nécessaire. Si l’utilisateur est connecté à votre application, la connexion SignalR hérite automatiquement de cette authentification.

Les cookies sont un moyen spécifique aux navigateurs pour transmettre des jetons d’accès, mais les clients non-navigateurs peuvent également les envoyer. Lorsque vous utilisez le client .NET, la propriété Cookies peut être configurée dans l’appel .WithUrl pour fournir un cookie. Toutefois, l’utilisation de l’authentification cookie à partir du client .NET nécessite que l’application fournisse une API pour échanger des données d’authentification pour un cookie.

Authentification des jetons du porteur

Le client peut fournir un jeton d’accès au lieu d’utiliser un cookie. Le serveur valide le jeton et l’utilise pour identifier l’utilisateur. Cette validation n’est effectuée que lorsque la connexion est établie. Pendant la durée de la connexion, le serveur ne vérifie pas automatiquement la révocation des jetons.

Dans le client JavaScript, le jeton peut être fourni à l’aide de l’option accessTokenFactory.

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

Dans le client .NET, il existe une propriété AccessTokenProvider similaire qui peut être utilisée pour configurer le jeton :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Note

La fonction de jeton d’accès que vous fournissez est appelée avant chaque requête HTTP effectuée par SignalR. Si vous devez renouveler le jeton afin de maintenir la connexion active (car elle peut expirer pendant la connexion), faites-le à partir de cette fonction et retournez le jeton mis à jour.

Dans les API web standard, les jetons d'accès sont envoyés dans un en-tête HTTP. Toutefois, SignalR ne peut pas définir ces en-têtes dans les navigateurs lorsque certains transports sont utilisés. Lors de l’utilisation de WebSockets et d’événements Server-Sent, le jeton est transmis en tant que paramètre de chaîne de requête.

Authentification JWT intégrée

Sur le serveur, l’authentification par jeton du porteur est configurée à l’aide de l’intergiciel JWT Bearer :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(options =>
        {
            // Identity made Cookie authentication the default.
            // However, we want JWT Bearer Auth to be the default.
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            // Configure the Authority to the expected value for your authentication provider
            // This ensures the token is appropriately validated
            options.Authority = /* TODO: Insert Authority URL here */;

            // We have to hook the OnMessageReceived event in order to
            // allow the JWT authentication handler to read the access
            // token from the query string when a WebSocket or 
            // Server-Sent Events request comes in.

            // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
            // due to a limitation in Browser APIs. We restrict it to only calls to the
            // SignalR hub in this code.
            // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
            // for more information about security considerations when using
            // the query string to transmit the access token.
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    // If the request is for our hub...
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/hubs/chat")))
                    {
                        // Read the token out of the query string
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSignalR();

    // Change to use Name as the user identifier for SignalR
    // WARNING: This requires that the source of your JWT token 
    // ensures that the Name claim is unique!
    // If the Name claim isn't unique, users could receive messages 
    // intended for a different user!
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

    // Change to use email as the user identifier for SignalR
    // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

    // WARNING: use *either* the NameUserIdProvider *or* the 
    // EmailBasedUserIdProvider, but do not use both. 
}

Note

La chaîne de requête est utilisée sur les navigateurs lors de la connexion avec des événements WebSocket et Server-Sent en raison des limitations de l’API du navigateur. Lorsque vous utilisez HTTPS, les valeurs de chaîne de requête sont sécurisées par la connexion TLS. Toutefois, de nombreux serveurs consignent les valeurs de chaîne de requête. Pour plus d'informations, consultez Considérations sur la sécurité dans ASP.NET Core SignalR. SignalR utilise des en-têtes pour transmettre des jetons dans des environnements qui les prennent en charge (tels que les clients .NET et Java).

Identity Authentification JWT du serveur

Lorsque vous utilisez IdentityServer, ajoutez un service PostConfigureOptions<TOptions> au projet :

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) && 
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Inscrivez le service dans Startup.ConfigureServices après avoir ajouté des services pour l'authentification (AddAuthentication) et le gestionnaire d'authentification pour Identity serveur (AddIdentityServerJwt) :

services.AddAuthentication()
    .AddIdentityServerJwt();
services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
        ConfigureJwtBearerOptions>());

Cookies par rapport aux jetons du porteur

Les cookies sont spécifiques aux navigateurs. L’envoi de ces jetons à partir d’autres types de clients ajoute de la complexité par rapport à l’envoi de jetons du porteur. En conséquent, l’authentification cookie n’est pas recommandée, sauf si l’application doit uniquement authentifier les utilisateurs à partir du client de navigateur. L’authentification par jeton du porteur est l’approche recommandée lors de l’utilisation de clients autres que le client de navigateur.

Authentification Windows

Si l’authentification Windows est configurée dans votre application, SignalR vous pouvez utiliser cette identité pour sécuriser les hubs. Toutefois, pour envoyer des messages à des utilisateurs individuels, vous devez ajouter un fournisseur d’ID utilisateur personnalisé. Le système d'authentification Windows ne fournit pas la revendication « Identifiant de nom ». SignalR utilise la revendication pour déterminer le nom d’utilisateur.

Ajoutez une nouvelle classe qui implémente IUserIdProvider et récupère l’une des revendications de l’utilisateur à utiliser comme identificateur. Par exemple, pour utiliser la revendication « Name » (qui est le nom d’utilisateur Windows au format [Domain]\[Username]), créez la classe suivante :

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Au lieu de ClaimTypes.Name, vous pouvez utiliser n’importe quelle valeur du User (par exemple, l’identificateur SID Windows, etc.).

Note

La valeur que vous choisissez doit être unique parmi tous les utilisateurs de votre système. Sinon, un message destiné à un utilisateur peut finir par être envoyé à un autre utilisateur.

Inscrivez ce composant dans votre méthode Startup.ConfigureServices.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services.AddSignalR();
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}

Dans le client .NET, l’authentification Windows doit être activée en définissant la propriété UseDefaultCredentials :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

L'authentification Windows est prise en charge dans Internet Explorer et Microsoft Edge, mais pas par tous les navigateurs. Par exemple, dans Chrome et Safari, la tentative d’utilisation de Authentification Windows et WebSockets échoue. Lorsque l'authentification Windows échoue, le client tente de se replier sur les autres transports qui peuvent fonctionner.

Utiliser des revendications pour personnaliser la gestion des identités

Une application qui authentifie les utilisateurs peut dériver SignalR des ID d’utilisateur à partir de déclarations utilisateur. Pour spécifier la façon dont SignalR crée les ID utilisateur, implémentez IUserIdProvider et inscrivez l’implémentation.

L’exemple de code montre comment utiliser les revendications pour sélectionner l’adresse e-mail de l’utilisateur comme propriété d’identification.

Note

La valeur que vous choisissez doit être unique parmi tous les utilisateurs de votre système. Sinon, un message destiné à un utilisateur peut finir par être envoyé à un autre utilisateur.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

L’inscription du compte ajoute une revendication avec type ClaimsTypes.Email à la base de données d’identité ASP.NET.

// create a new user
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

// add the email claim and value for this user
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

Enregistrez ce composant dans votre Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autoriser les utilisateurs à accéder aux hubs et aux méthodes des hubs

Par défaut, toutes les méthodes d'un hub peuvent être appelées par un utilisateur non authentifié. Pour exiger l'authentification, appliquez l'attribut AuthorizeAttribute au concentrateur :

[Authorize]
public class ChatHub: Hub
{
}

Vous pouvez utiliser les arguments du constructeur et les propriétés de l’attribut [Authorize] pour restreindre l’accès aux seuls utilisateurs correspondant à des stratégies d’autorisation spécifiques. Par exemple, si vous avez une stratégie d’autorisation personnalisée appelée MyAuthorizationPolicy , vous pouvez vous assurer que seuls les utilisateurs correspondant à cette stratégie peuvent accéder au hub à l’aide du code suivant :

[Authorize("MyAuthorizationPolicy")]
public class ChatHub : Hub
{
}

L’attribut [Authorize] peut également être appliqué à des méthodes de hub individuelles. Si l’utilisateur actuel ne correspond pas aux stratégies appliquées à la méthode, une erreur est retournée à l’appelant :

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Utiliser des gestionnaires d’autorisation pour personnaliser l’autorisation de méthode hub

SignalR fournit une ressource personnalisée aux gestionnaires d’autorisation lorsqu’une méthode hub nécessite une autorisation. La ressource est une instance de HubInvocationContext. Le HubInvocationContext inclut le HubCallerContext, le nom de la méthode hub appelée et les arguments de la méthode hub.

Prenons l’exemple d’une salle de conversation autorisant la connexion à plusieurs organisations par le biais de Microsoft Entra ID. Toute personne disposant d’un compte Microsoft peut se connecter à la conversation, mais seuls les membres de l’organisation propriétaire devraient pouvoir interdire les utilisateurs ou afficher les historiques de conversation des utilisateurs. En outre, nous pourrions vouloir restreindre certaines fonctionnalités de certains utilisateurs. L’utilisation des fonctionnalités mises à jour dans ASP.NET Core 3.0 est tout à fait possible. Observez comment DomainRestrictedRequirement fonctionne en tant que IAuthorizationRequirement personnalisé. Maintenant que le paramètre HubInvocationContext de ressource est transmis, la logique interne peut inspecter le contexte dans lequel le hub est appelé et prendre des décisions sur l’autorisation de l’utilisateur d’exécuter des méthodes Hub individuelles.

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

public class DomainRestrictedRequirement : 
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>, 
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement, 
        HubInvocationContext resource)
    {
        if (IsUserAllowedToDoThis(resource.HubMethodName, context.User.Identity.Name) && 
            context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") && 
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

Dans Startup.ConfigureServices, ajoutez la nouvelle stratégie, en fournissant l’exigence personnalisée DomainRestrictedRequirement en tant que paramètre pour créer la stratégie DomainRestricted.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services
        .AddAuthorization(options =>
        {
            options.AddPolicy("DomainRestricted", policy =>
            {
                policy.Requirements.Add(new DomainRestrictedRequirement());
            });
        });
}

Dans l’exemple précédent, la classe DomainRestrictedRequirement est à la fois un IAuthorizationRequirement et son propre AuthorizationHandler pour cette exigence. Il est acceptable de diviser ces deux composants en classes distinctes pour séparer les préoccupations. L’un des avantages de l’approche de l’exemple est qu’il n’est pas nécessaire d’injecter le AuthorizationHandler lors du démarrage, car l’exigence et le gestionnaire sont identiques.

Ressources supplémentaires