Partage via


Authentification et autorisation dans ASP.NET Core 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();

Notes

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 ServerSentEvent échouent lors des demandes suivantes si elles n’envoient pas de nouveaux jetons d’accès. Pour que les connexions se ferment à l’expiration du jeton d’authentification, définissez CloseOnAuthenticationExpiration.

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. Lors de l’utilisation du client de navigateur, aucune configuration supplémentaire n’est nécessaire. Si l’utilisateur est connecté à une 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 revalide pas automatiquement pour case activée pour 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();

Notes

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

Dans les API web standard, les jetons du porteur sont envoyés dans un en-tête HTTP. Toutefois, SignalR ne peut pas définir ces en-têtes dans les navigateurs lors de l’utilisation de certains transports. 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 :

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();

Notes

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 Duende 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 après avoir ajouté des services pour l’authentification (AddAuthentication) et le gestionnaire d’authentification pour Identity le serveur (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 et 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 peut utiliser cette identity pour sécuriser les hubs. Toutefois, pour envoyer des messages à des utilisateurs individuels, ajoutez un fournisseur d’ID utilisateur personnalisé. Le système Authentification Windows ne fournit pas la revendication « Identificateur 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 de User, telle que l’identificateur SID Windows, etc.

Notes

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

Inscrivez ce composant dans 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, 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();

Authentification Windows est pris 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 Authentification Windows échoue, le client tente de revenir à d’autres transports qui peuvent fonctionner.

Utiliser des revendications pour personnaliser la gestion des identity

Une application qui authentifie les utilisateurs peut dériver SignalR des ID d’utilisateur à partir de revendications 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.

Notes

La valeur choisie doit être unique parmi tous les utilisateurs du 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 de compte ajoute une revendication de type ClaimsTypes.Email à la base de données d’identity 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 Program.cs :

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

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

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
{
}

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 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 d’utilisateurs spécifiques. Notez comment DomainRestrictedRequirement sert de 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)
    {
    }
}

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 Program.cs, ajoutez la nouvelle stratégie, en fournissant l’exigence personnalisée DomainRestrictedRequirement en tant que paramètre pour créer la stratégie DomainRestricted :

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’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

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?}");
    });
}

Notes

Si un jeton expire pendant la durée de vie d’une connexion, la connexion continue de fonctionner. Les connexions LongPolling et ServerSentEvent é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 revalide pas automatiquement pour case activée pour 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();

Notes

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 du porteur sont envoyés dans un en-tête HTTP. Toutefois, SignalR ne peut pas définir ces en-têtes dans les navigateurs lors de l’utilisation de certains transports. 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. 
}

Si vous souhaitez voir les commentaires de code traduits dans une langue autre que l’anglais, dites-le nous dans cette discussion GitHub.

Notes

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 dan sStartup.ConfigureServices après avoir ajouté des services pour l’authentification (AddAuthentication) et le gestionnaire d’authentification pour Identity le serveur (AddIdentityServerJwt) :

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

Cookies et 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 peut utiliser cette identity 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 Authentification Windows ne fournit pas la revendication « Identificateur 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.).

Notes

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();

Authentification Windows est pris en charge dans Internet Explorer et 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 Authentification Windows échoue, le client tente de revenir à d’autres transports qui peuvent fonctionner.

Utiliser des revendications pour personnaliser la gestion des identity

Une application qui authentifie les utilisateurs peut dériver SignalR des ID d’utilisateur à partir de revendications 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.

Notes

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 de compte ajoute une revendication de type ClaimsTypes.Email à la base de données d’identity 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));

Inscrivez ce composant dans votre Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

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

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 peut également être appliqué à des [Authorize] 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. Notez comment DomainRestrictedRequirement sert de 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