ASP.NET Core SignalR使用Azure AD返回401未经授权

时间:2019-08-28 08:49:59

标签: c# angular asp.net-core azure-active-directory signalr

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

一切正常,但是我也将SignalR与API一起用作服务器,当我尝试从SPA连接时,我在协商“请求”中获得401未经授权,然后在响应标题中获得了此信息:

Response Header

该请求在Authorization标头中包含我的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);
    }
}

这是我的角度客户端上的SignalRService。我正在app.component.ts的构造函数中运行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也会停止处理常规请求,并且我得到了回报:

Signature key was not found

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

/ EDIT

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

3 个答案:

答案 0 :(得分:1)

只需看看官方的docs。您需要对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 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. 
}

答案 1 :(得分:1)

在验证访问令牌的签名时,您应该获取公共密钥,因为Azure AD可以使用一组特定的公共-私有密钥对中的任何一个对令牌进行签名,可以在以下位置找到密钥:

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

在JSON响应中,您将看到属性jwks_uri,该属性是URI,其中包含Azure AD的JSON Web密钥集。匹配jwt令牌中的kid声明,您可以找到AAD用于通过非对称加密算法(默认情况下为RSA 256)对令牌进行签名的密钥。

在asp.net核心api中,当验证Azure AD发出的访问令牌时,可以使用AddJwtBearer扩展名并提供正确的Authority,以便中间件将正确地从Azure获取密钥。 AD OpenID配置终结点:

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

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

答案 2 :(得分:0)

将中心上的Authorize属性更改为

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]