ASP.NET Core SignalR returns 401 未授权使用 Azure AD

ASP.NET Core SignalR returns 401 Unauthorized using Azure AD

我有一个 SPA (angular 7) 和一个 API (.Net Core),我使用 Azure AD 对其进行了身份验证。我正在使用 adal-angular4 将我的 angular 应用程序与 AAD 集成。

一切正常,但我也在使用带有 API 的 SignalR 作为服务器,当我尝试从我的 SPA 连接时,我在协商 "request" 时收到 401 Unauthorized 并且我得到了这个返回响应 Headers:

请求在授权header中包含我的Bearer令牌,当我通过jwt.io运行令牌时,我可以看到"aud"值是我的 SPA 的 Azure AD ClientId。

对 API 的所有常规请求都包含相同的标记,我对这些没有任何问题。我的所有控制器和集线器上都有 [授权],但只有 SignalR 集线器导致了这个问题。

我的服务器启动:

public Startup(IConfiguration configuration, IHostingEnvironment env)
{
    Configuration = configuration;
    _env = env;
}

public IConfiguration Configuration { get; }
private IHostingEnvironment _env;
public void ConfigureServices(IServiceCollection services)
{

    StartupHandler.SetupDbContext(services, Configuration.GetConnectionString("DevDb"));


    // Setup Authentication
    services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
        .AddAzureADBearer(options =>
        {
            Configuration.Bind("AzureAD", options);


        });

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

    // Add functionality to inject IOptions<T>
    services.AddOptions();

    // Add AzureAD object so it can be injected
    services.Configure<AzureAdConfig>(Configuration.GetSection("AzureAd"));

    services.AddSignalR(options =>
    {
        options.EnableDetailedErrors = true;
        options.KeepAliveInterval = TimeSpan.FromSeconds(10);
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseDeveloperExceptionPage();
        app.UseHsts();
    }

    app.UseCookiePolicy();

    app.UseHttpsRedirection();

    //app.UseCors("AllowAllOrigins");
    app.UseCors(builder =>
    {
        builder.AllowAnyOrigin();
        builder.AllowAnyMethod().AllowAnyHeader();
        builder.AllowCredentials();
    });


    app.UseAuthentication();

    app.UseSignalR(routes => routes.MapHub<MainHub>("/mainhub"));

    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(_env.ContentRootPath, "Files")),
        RequestPath = new PathString("/Files")
    });

    app.UseMvc();
}

我的 SignalR 中心:

[Authorize]
public class MainHub : Hub
{
    private readonly IEntityDbContext _ctx;

    public MainHub(IEntityDbContext ctx)
    {
        _ctx = ctx;
        _signalRService = signalRService;
    }

    public override Task OnConnectedAsync()
    {
        return base.OnConnectedAsync();
    }

    public override Task OnDisconnectedAsync(Exception exception)
    {
        return base.OnDisconnectedAsync(exception);
    }
}

这是我 angular 客户端上的 SignalRService。我在 app.component.ts.

的构造函数中 运行ning startConnection()
export class SignalRService {
    private hubConnection: signalR.HubConnection;

    constructor(private adal: AdalService) {}

    startConnection(): void {
        this.hubConnection = new signalR.HubConnectionBuilder()
            .withUrl(AppConstants.SignalRUrl, { accessTokenFactory: () => this.adal.userInfo.token})
            .build();

        this.hubConnection.serverTimeoutInMilliseconds = 60000;

        this.hubConnection.on('userConnected', (user) => 
        {
            console.log(user);
        });

        this.hubConnection.start()
            .then(() => console.log('Connection started'))
            .catch(err => 
            {
                console.log('Error while starting connection: ' + err);
            });
    }
}

我试过 this 解决方案,但我也无法使用它。

编辑

当我从官方文档实施解决方案时,API 也停止处理常规请求,我回来了:

我用 new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); 填充了 TokenValidationParameters 中的 IssuerSigningKey 属性。我做错了什么吗?

/编辑

为什么 SignalR 不接受我的访问令牌,而 API 否则接受它?

看看官方就知道了docs。您需要对 JWT Bearer 事件进行特殊处理,以便您的身份验证正常工作。令牌需要转发到集线器。 看看我说的部分

THAT PART IS MISSING

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 JWT Bearer Auth to expect our security key
            options.TokenValidationParameters =
                new TokenValidationParameters
                {
                    LifetimeValidator = (before, expires, token, param) =>
                    {
                        return expires > DateTime.UtcNow;
                    },
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateActor = false,
                    ValidateLifetime = true,
                    IssuerSigningKey = SecurityKey
                };

            //THAT IS THE PART WHICH IS MISSING IN YOUR CONFIG !
            // 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.
            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. 
}

将集线器上的授权属性更改为

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

验证访问令牌的签名时,您应该获得 public 密钥,因为 Azure AD 可能使用一组特定的 public-私钥对中的任何一个来签署令牌,密钥可以位于:

https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration 

在 JSON 响应中,您会看到 属性 jwks_uri 这是包含 Azure AD 的 JSON Web 密钥集的 URI。匹配 jwt token 中的 kid 声明,可以找到 AAD 使用非对称加密算法对令牌进行签名的密钥,例如默认使用 RSA 256。

在asp.net core apis中,验证Azure AD颁发的访问令牌时,可以使用AddJwtBearer扩展并提供正确的Authority,以便中间件正确获取来自 Azure AD OpenID 配置终结点的密钥:

options.Authority = "https://login.microsoftonline.com/yourtenant.onmicrosoft.com/"

另一种选择是使用 Microsoft.AspNetCore.Authentication.AzureAD.UI 库的 AddAzureADBearer 扩展。您还应该设置正确的 authority(instance + domain) ,中间件将根据您的配置帮助验证签名和声明。