ASP.NET Core SignalR での認証と認可

SignalR ハブに接続するユーザーを認証する

SignalR を ASP.NET Core 認証と共に使用して、ユーザーを各接続に関連付けることができます。 ハブでは、認証データにプロパティから HubConnectionContext.User アクセスできます。 認証を使うと、ハブではユーザーに関連付けられているすべての接続でメソッドを呼び出せます。 詳細については、「SignalR でユーザーとグループを管理する」を参照してください。 複数の接続を 1 人のユーザーに関連付けることができます。

次のコードは、認証を使用SignalRして 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

接続の有効期間中にトークンの有効期限が切れた場合、既定では接続は引き続き機能します。 LongPolling および ServerSentEvent 接続は、新しいアクセス トークンを送信しなければ、後続の要求で失敗します。 認証トークンの有効期限が切れたときに接続を閉じるには、CloseOnAuthenticationExpiration を設定します。

ブラウザーベースのアプリでは、認証により、 cookie 既存のユーザー資格情報を接続に自動的に SignalR フローできます。 ブラウザー クライアントを使用する場合は、追加の構成は必要ありません。 ユーザーがアプリにログインしている場合、 SignalR 接続はこの認証を自動的に継承します。

Cookie はアクセス トークンを送信するためのブラウザー固有の方法ですが、ブラウザー以外のクライアントでも送信できます。 .NET クライアントを使う場合、.WithUrl 呼び出しで Cookies プロパティを構成して cookie を提供できます。 ただし、.NET クライアントからの cookie 認証を使うには、cookie の認証データを交換するための API をアプリで提供する必要があります。

ベアラー トークン認証

クライアントでは、cookie を使う代わりにアクセス トークンを提供できます。 サーバーはトークンを検証し、それを使用してユーザーを識別します。 この検証は、接続が確立されている場合にのみ行われます。 接続の期間中、サーバーによってトークンの失効が自動的に再検証されることはありません。

JavaScript クライアントでは、accessTokenFactory オプションを使ってトークンを提供できます。

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

.NET クライアントには、トークンの構成に使用できる同様の AccessTokenProvider プロパティがあります。

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

Note

指定されたアクセス トークン関数は、によって行われる すべての HTTP 要求の前に SignalR呼び出されます。 接続をアクティブに保つためにトークンを更新する必要がある場合は、この関数内から更新し、更新されたトークンを返します。 接続中にトークンの有効期限が切れないよう、トークンを更新する必要がある場合があります。

標準的な Web API では、ベアラー トークンは HTTP ヘッダーで送信されます。 ただし、一部のトランスポートを使う場合、SignalR ではこれらのヘッダーをブラウザーで設定できません。 WebSockets と Server-Sent Events を使う場合、トークンはクエリ文字列パラメーターとして送信されます。

組み込みの JWT 認証

サーバーでは、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 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

ブラウザー API の制限により、クエリ文字列は WebSockets と Server-Sent Events に接続するときにブラウザーで使用されます。 HTTPS を使用する場合、クエリ文字列値は TLS 接続によって保護されます。 ただし、多くのサーバーではクエリ文字列値がログに記録されます。 詳細については、「セキュリティに関する考慮事項 ASP.NET Core SignalR」を参照してください。 SignalR では、ヘッダーを使って、それらをサポートする環境 (.NET や Java クライアントなど) でトークンを送信します。

Identity Server の JWT 認証

Duende IdentityServer を使用する場合は、プロジェクトに PostConfigureOptions<TOptions> サービスを追加します。

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

認証用のサービス () とサーバーAddIdentityServerJwt (AddAuthentication) の認証ハンドラーを追加した後、サービスをIdentity登録します。

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.

Cookie とベアラー トークン

Cookie はブラウザーに固有のものです。 それを他の種類のクライアントから送信する場合は、ベアラー トークンの送信と比較して複雑になります。 Cookie 認証は、アプリがブラウザー クライアントからユーザーを認証する必要がある場合を除き、推奨されません。 ベアラー トークン認証は、ブラウザー クライアント以外のクライアントを使う場合に推奨される方法です。

Windows 認証

Windows 認証がアプリで構成されている場合は、SignalRその ID を使用してハブをセキュリティで保護できます。 ただし、個々のユーザーにメッセージを送信するには、カスタム ユーザー ID プロバイダーを追加します。 Windows 認証システムでは、"Name Identifier" クレームは提供されません。 SignalR では、クレームを使ってユーザー名を判別します。

IUserIdProvider を実装する新しいクラスを追加して、識別子として使ういずれかのクレームをユーザーから取得します。 たとえば、"Name" クレーム ([Domain]/[Username] という形式の Windows ユーザー名) を使うには、次のクラスを作成します。

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

ではなく ClaimTypes.Name、Windows SID 識別子などの任意の User値を使用します。

Note

選択する値は、システム内のすべてのユーザー間で一意である必要があります。 そうしないと、1 人のユーザーを対象としたメッセージが別のユーザーに送信される可能性があります。

このコンポーネントを次の中に 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.

.NET クライアントでは、プロパティを設定して Windows 認証を UseDefaultCredentials 有効にする必要があります。

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

Windows 認証は Microsoft Edge でサポートされていますが、一部のブラウザーではサポートされていません。 たとえば、Chrome と Safari では、Windows 認証や WebSockets を使おうとすると失敗します。 Windows 認証が失敗したとき、クライアントでは動作する可能性がある他のトランスポートへのフォール バックが試みられます。

クレームを使って ID 処理をカスタマイズする

ユーザーを認証するアプリでは、ユーザー クレームから SignalR ユーザー ID を派生できます。 SignalR によるユーザー ID の作成方法を指定するには、IUserIdProvider を実装し、その実装を登録します。

サンプル コードでは、要求を使用して、識別プロパティとしてユーザーの電子メール アドレスを選択する方法を示します。

Note

選択する値は、システム内のすべてのユーザー間で一意である必要があります。 そうしないと、1 人のユーザーを対象としたメッセージが別のユーザーに送信される可能性があります。

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

アカウント登録では、型 ClaimsTypes.Email を持つクレームが ASP.NET ID データベースに追加されます。

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.

このコンポーネントを次の中に Program.cs登録します。

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

ユーザーに対してハブとハブ メソッドへのアクセスを認可する

既定では、認証されていないユーザーがハブ内のすべてのメソッドを呼び出すことができます。 認証を要求するには、ハブに属性を AuthorizeAttribute 適用します。

[Authorize]
public class ChatHub: Hub
{
}

属性のコンストラクター引数とプロパティを [Authorize] 使用して、特定の 承認ポリシーに一致するユーザーのみにアクセスを制限できます。 たとえば、カスタム承認ポリシーを呼び出 MyAuthorizationPolicyした場合、そのポリシーに一致するユーザーのみが、次のコードを使用してハブにアクセスできます。

[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.

属性は [Authorize] 、個々のハブ メソッドに適用できます。 現在のユーザーがメソッドに適用されているポリシーと一致しない場合は、呼び出し元にエラーが返されます。

[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) ...
    }
}

認可ハンドラーを使ってハブ メソッドの認可をカスタマイズする

SignalR では、ハブ メソッドに認可が必要な場合に、認可ハンドラーに対してカスタム リソースが提供されます。 リソースは HubInvocationContext のインスタンスです。 HubInvocationContext には、HubCallerContext、呼び出されているハブ メソッドの名前、ハブ メソッドへの引数が含まれます。

複数の組織が Azure Active Directory を使ってサインインできるチャット ルームの例について考えてみます。 Microsoft アカウントを持つユーザーは誰でもサインインしてチャットできますが、ユーザーを利用禁止にしたりユーザーのチャット履歴を表示したりできるのは、所有している組織のメンバーのみにする必要があります。 さらに、特定のユーザーから一部の機能を制限することもできます。 DomainRestrictedRequirement がカスタムの IAuthorizationRequirement として機能するしくみに注意してください。 リソース パラメーターが HubInvocationContext 渡されたので、内部ロジックはハブが呼び出されているコンテキストを検査し、ユーザーが個々の Hub メソッドを実行できるようにすることを決定できます。

[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));
    }
}

Program.cs、新しいポリシーを追加し、ポリシーを作成するためのパラメーターとしてカスタム DomainRestrictedRequirement 要件を 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.

上記の例では、DomainRestrictedRequirement クラスは IAuthorizationRequirement とその要件に対する独自の AuthorizationHandler クラスの両方です。 これら 2 つのコンポーネントを別々のクラスに分割して、懸念事項を分けることもできます。 この例の方法の利点は、要件とハンドラーが同じものであるため、起動時に AuthorizationHandler を挿入する必要がないことです。

その他の技術情報

サンプル コードを表示またはダウンロードする (ダウンロード方法)

SignalR ハブに接続するユーザーを認証する

SignalR を ASP.NET Core 認証と共に使用して、ユーザーを各接続に関連付けることができます。 ハブでは、プロパティから認証データに HubConnectionContext.User アクセスできます。 認証を使うと、ハブではユーザーに関連付けられているすべての接続でメソッドを呼び出せます。 詳細については、「SignalR でユーザーとグループを管理する」を参照してください。 複数の接続を 1 人のユーザーに関連付けることができます。

以下に、SignalR と ASP.NET Core 認証を使用する Startup.Configure の例を示します。

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

接続の有効期間中にトークンの有効期限が切れた場合、接続は引き続き機能します。 LongPolling および ServerSentEvent 接続は、新しいアクセス トークンを送信しなければ、後続の要求で失敗します。

ブラウザー ベースのアプリでは、cookie 認証を使うと、既存のユーザー資格情報を自動的に SignalR 接続に送信できます。 ブラウザー クライアントを使う場合、追加の構成は必要ありません。 ユーザーがアプリにログインしている場合、SignalR 接続では自動的にこの認証を継承します。

Cookie はアクセス トークンを送信するためのブラウザー固有の方法ですが、ブラウザー以外のクライアントでも送信できます。 .NET クライアントを使う場合、.WithUrl 呼び出しで Cookies プロパティを構成して cookie を提供できます。 ただし、.NET クライアントからの cookie 認証を使うには、cookie の認証データを交換するための API をアプリで提供する必要があります。

ベアラー トークン認証

クライアントでは、cookie を使う代わりにアクセス トークンを提供できます。 サーバーはトークンを検証し、それを使用してユーザーを識別します。 この検証は、接続が確立されている場合にのみ行われます。 接続の期間中、サーバーによってトークンの失効が自動的に再検証されることはありません。

JavaScript クライアントでは、accessTokenFactory オプションを使ってトークンを提供できます。

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

.NET クライアントには、トークンの構成に使用できる同様の AccessTokenProvider プロパティがあります。

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

Note

指定したアクセス トークン関数は、SignalR によって行われるすべての HTTP 要求よりも前に呼び出されます。 接続をアクティブな状態に保つためにトークンを更新する必要がある場合 (接続中に期限切れになる可能性があるため) は、この関数内から実行し、更新されたトークンを返します。

標準的な Web API では、ベアラー トークンは HTTP ヘッダーで送信されます。 ただし、一部のトランスポートを使う場合、SignalR ではこれらのヘッダーをブラウザーで設定できません。 WebSockets と Server-Sent Events を使う場合、トークンはクエリ文字列パラメーターとして送信されます。

組み込みの JWT 認証

サーバーでは、JWT ベアラー ミドルウェアを使ってベアラー トークン認証が構成されます。

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

コードのコメントを英語以外の言語に翻訳し表示したい場合、こちらの GitHub ディスカッション イシューにてお知らせください。

Note

ブラウザー API の制限により、クエリ文字列は WebSockets と Server-Sent Events に接続するときにブラウザーで使用されます。 HTTPS を使用する場合、クエリ文字列値は TLS 接続によって保護されます。 ただし、多くのサーバーではクエリ文字列値がログに記録されます。 詳細については、「セキュリティに関する考慮事項 ASP.NET Core SignalR」を参照してください。 SignalR では、ヘッダーを使って、それらをサポートする環境 (.NET や Java クライアントなど) でトークンを送信します。

Identity Server の JWT 認証

Identity サーバーを使う場合は、PostConfigureOptions<TOptions> サービスをプロジェクトに追加します。

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

認証用のサービス (AddAuthentication) と Identity Server の認証ハンドラー (AddIdentityServerJwt) を追加した後に、Startup.ConfigureServices にサービスを登録します。

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

Cookie とベアラー トークン

Cookie はブラウザーに固有のものです。 それを他の種類のクライアントから送信する場合は、ベアラー トークンの送信と比較して複雑になります。 そのため、アプリでブラウザー クライアントからのユーザーのみを認証すればよい場合を除いて、cookie 認証は推奨されません。 ベアラー トークン認証は、ブラウザー クライアント以外のクライアントを使う場合に推奨される方法です。

Windows 認証

アプリで Windows 認証が構成されている場合、SignalR ではその ID を使ってハブをセキュリティで保護できます。 ただし、個々のユーザーにメッセージを送信するには、カスタム ユーザー ID プロバイダーを追加する必要があります。 Windows 認証システムでは、"Name Identifier" クレームは提供されません。 SignalR では、クレームを使ってユーザー名を判別します。

IUserIdProvider を実装する新しいクラスを追加して、識別子として使ういずれかのクレームをユーザーから取得します。 たとえば、"Name" クレーム ([Domain]\[Username] という形式の Windows ユーザー名) を使うには、次のクラスを作成します。

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

ClaimTypes.Name の代わりに、User の任意の値 (たとえば Windows SID 識別子など) を使用できます。

Note

選ぶ値は、システム内のすべてのユーザー間で一意である必要があります。 そうしないと、1 人のユーザーを対象としたメッセージが別のユーザーに送信される可能性があります。

次のコンポーネントを Startup.ConfigureServices メソッドに登録します。

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

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

.NET クライアントでは、プロパティを設定して Windows 認証を UseDefaultCredentials 有効にする必要があります。

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

Windows 認証は Internet Explorer と Microsoft Edge でサポートされていますが、一部のブラウザーではサポートされていません。 たとえば、Chrome と Safari では、Windows 認証や WebSockets を使おうとすると失敗します。 Windows 認証が失敗したとき、クライアントでは動作する可能性がある他のトランスポートへのフォール バックが試みられます。

クレームを使って ID 処理をカスタマイズする

ユーザーを認証するアプリでは、ユーザー クレームから SignalR ユーザー ID を派生できます。 SignalR によるユーザー ID の作成方法を指定するには、IUserIdProvider を実装し、その実装を登録します。

次のサンプル コードでは、クレームを使ってユーザーのメール アドレスを識別プロパティとして選ぶ方法が示されています。

Note

選ぶ値は、システム内のすべてのユーザー間で一意である必要があります。 そうしないと、1 人のユーザーを対象としたメッセージが別のユーザーに送信される可能性があります。

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

アカウント登録では、型 ClaimsTypes.Email を持つクレームが ASP.NET ID データベースに追加されます。

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

次のコンポーネントを Startup.ConfigureServices に登録します。

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

ユーザーに対してハブとハブ メソッドへのアクセスを認可する

既定では、認証されていないユーザーがハブ内のすべてのメソッドを呼び出すことができます。 認証を要求するには、ハブに AuthorizeAttribute 属性を適用します。

[Authorize]
public class ChatHub: Hub
{
}

コンストラクターの引数と [Authorize] 属性のプロパティを使用して、特定の承認ポリシーに一致するユーザーのみにアクセスを制限できます。 たとえば、MyAuthorizationPolicy というカスタム認可ポリシーがある場合は、次のコードを使って、そのポリシーに一致するユーザーだけがハブにアクセスできるようにすることができます。

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

個々のハブ メソッドに [Authorize] 属性を適用することもできます。 現在のユーザーがメソッドに適用されているポリシーと一致しない場合は、呼び出し元にエラーが返されます。

[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) ...
    }
}

認可ハンドラーを使ってハブ メソッドの認可をカスタマイズする

SignalR では、ハブ メソッドに認可が必要な場合に、認可ハンドラーに対してカスタム リソースが提供されます。 リソースは HubInvocationContext のインスタンスです。 HubInvocationContext には、HubCallerContext、呼び出されているハブ メソッドの名前、ハブ メソッドへの引数が含まれます。

複数の組織が Azure Active Directory を使ってサインインできるチャット ルームの例について考えてみます。 Microsoft アカウントを持つユーザーは誰でもサインインしてチャットできますが、ユーザーを利用禁止にしたりユーザーのチャット履歴を表示したりできるのは、所有している組織のメンバーのみにする必要があります。 さらに、特定のユーザーに対して特定の機能を制限したい場合もあります。 ASP.NET Core 3.0 で更新された機能を使えば、これを完全に実現できます。 DomainRestrictedRequirement がカスタムの IAuthorizationRequirement として機能するしくみに注意してください。 HubInvocationContext リソース パラメーターが渡されているので、内部ロジックではハブが呼び出されているコンテキストを検査して、ユーザーが個々のハブ メソッドを実行できるようにするかどうかを決定できます。

[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));
    }
}

Startup.ConfigureServices に新しいポリシーを追加し、DomainRestricted ポリシーを作成するためのパラメーターとしてカスタム DomainRestrictedRequirement 要件を指定します。

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

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

上記の例では、DomainRestrictedRequirement クラスは IAuthorizationRequirement とその要件に対する独自の AuthorizationHandler クラスの両方です。 これら 2 つのコンポーネントを別々のクラスに分割して、懸念事項を分けることもできます。 この例の方法の利点は、要件とハンドラーが同じものであるため、起動時に AuthorizationHandler を挿入する必要がないことです。

その他の技術情報