使用(Azure Active Directory B2C)Json Web令牌验证的ASP.NET SignalR客户端-服务器身份验证

时间:2019-01-21 17:52:42

标签: asp.net signalr azure-ad-b2c

我有两个应用程序……一个是JavaScript signalR客户端,另一个是asp.net Web应用程序,用作SignalR服务器,将更新广播到客户端。我试图使用azure活动目录b2c服务通过客户端应用程序为用户提供身份验证和授权,以访问服务器中的资源。因此,在令牌验证之后,只有经过身份验证的JavaScript客户端用户才能启动与托管SignalR服务器的asp.net Web应用程序的signalR连接。 由于signalR使用网络套接字,因此我们无法在HTTP连接请求标头中提供令牌。看来我应该使用查询字符串在signalR连接请求中提供身份验证令牌。 在asp.net服务器应用程序中收到该令牌后,我需要验证该令牌并允许JavaScript客户端应用程序具有signalR连接。 我想在此博客文章https://kwilson.io/blog/authorize-your-azure-ad-users-with-signalr/中实现完全相同的操作,但要使用azure活动目录b2c。

1 个答案:

答案 0 :(得分:0)

似乎其他人在使用ASP.NET SignalR Client和服务器体系结构时也可能遇到相同的问题。 实际上,通过大量的努力,我能够通过自定义signalR集线器的AuthorizeModule来解决此问题。实际上,我使用CustomAuthorization类中的AuthorizeAttribute继承重写AuthorizeHubConnection()和AuthorizeHubMethodInvocation()。 首先,我在启动配置中的app.Map(“ / signalr”,map => {....}中添加了GlobalHost.HubPipeline.AddModule(module)。您可以在以下startup.cs中看到它。

using Microsoft.Owin;
using Microsoft.Owin.Cors;
using Owin;
using Microsoft.AspNet.SignalR;
using TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration;
using Microsoft.AspNet.SignalR.Hubs;

[assembly: OwinStartup(typeof(TestCarSurveillance.RealTimeCommunication.Startup))]

namespace TestCarSurveillance.RealTimeCommunication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            //After adding Authorization module in GlobalHost.HubPipeline.AddModule(module)
            //program was unable to create the log file so I have added it.
            log4net.Config.XmlConfigurator.Configure();

            // Branch the pipeline here for requests that start with "/signalr"
            //app.UseWelcomePage("/");
            app.Map("/signalr", map =>
            {
                // Setup the CORS middleware to run before SignalR.
                // By default this will allow all origins. You can 
                // configure the set of origins and/or http verbs by
                // providing a cors options with a different policy.

                map.UseCors(CorsOptions.AllowAll);
                var hubConfiguration = new HubConfiguration
                {
                    EnableDetailedErrors = true,
                    // You can enable JSONP by uncommenting line below.
                    // JSONP requests are insecure but some older browsers (and some
                    // versions of IE) require JSONP to work cross domain
                    EnableJSONP = true
                };

                // Require authentication for all hubs
                var authorizer = new CustomAuthorization();
                var module = new AuthorizeModule(authorizer, authorizer);
                GlobalHost.HubPipeline.AddModule(module);

                map.RunSignalR(hubConfiguration);
            });
        }

    }
}

此Authorize模块在每个signalR集线器OnConnected(),OnDisconnected(),OnReconnected()和客户端可以调用的集线器方法中调用CustomAuthorize.cs类。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.AspNet.SignalR.Owin;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security.Jwt;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;

namespace TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration
{

    public class CustomAuthorization : AuthorizeAttribute
    {
        // These values are pulled from web.config for b2c authorization
        public static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
        public static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
        public static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        public static string signUpInPolicy = ConfigurationManager.AppSettings["ida:SignUpInPolicyId"];

        static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        //This method is called multiple times before the connection with signalR is established.
        public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
        {
            var metadataEndpoint = string.Format(aadInstance, tenant, signUpInPolicy);
            // Extract JWT token from query string.
            var userJwtToken = request.QueryString.Get("Authorization");
            if (string.IsNullOrEmpty(userJwtToken))
            {
                return false;
            }

            // Validate JWT token.
            //var tokenValidationParameters = new TokenValidationParameters { ValidAudience = ClientId };
            //Contains a set of parameters that are used by a SecurityTokenHandler when validating a SecurityToken.
            TokenValidationParameters tvps = new TokenValidationParameters
            {
                // Accept only those tokens where the audience of the token is equal to the client ID of this app
                // This is where you specify that your API only accepts tokens from its own clients
                // here the valid audience is supplied to check against the token's audience
                ValidAudience = clientId,
                ValidateIssuer = false,
                // It is the authentication scheme used for token validation
                AuthenticationType = signUpInPolicy,
                //SaveSigninToken = true,

                //I’ve configured the “NameClaimType” of the “TokenValidationParameters” to use the claim named “objectidentifer” (“oid”) 
                //This will facilitate reading the unique user id for the authenticated user inside the controllers, all we need to call 
                //now inside the controller is: “User.Identity.Name” instead of querying the claims collection each time

                //Gets or sets a String that defines the NameClaimType.
                NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier"
            };
            try
            {
                var jwtFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint));
                var authenticationTicket = jwtFormat.Unprotect(userJwtToken);

                if(authenticationTicket != null && authenticationTicket.Identity !=null && authenticationTicket.Identity.IsAuthenticated)
                {
                    var email = authenticationTicket.Identity.FindFirst(p => p.Type == "emails").Value;

                    // It is done to call the async method from sync method 
                    //the ArgumentException will be caught as you’d expect, because .GetAwaiter().GetResult() unrolls the first exception the same way await does. 
                    //This approach follows the principle of least surprise and is easier to understand.
                    // set the authenticated user principal into environment so that it can be used in the future
                    request.Environment["server.User"] = new ClaimsPrincipal(authenticationTicket.Identity);

                    return true;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
                log.Error(ex);
                //throw ex;

            }

            return false;
        }

        public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
        {
            var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
            //Check the authenticated user principal from environment
            var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
            //ClaimsPrincipal supports multiple claims based identities
            var principal = environment["server.User"] as ClaimsPrincipal;
            if(principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
            {
                    // create a new HubCallerContext instance with the principal generated from token
                    // and replace the current context so that in hubs we can retrieve current user identity
                    hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);
                    return true;
            }
            return false;          
        }
    }
}

从查询字符串中收到令牌后,我们需要设置TokenValidationParameters在metadataEndpoint中使用它来进行令牌验证。令牌验证是在建立集线器连接之前完成的,这样只有授权用户才能建立连接,如果连接不成功,它将返回401响应。在OpenIdConnectCachingSecurityTokenProvider.cs类中实现。此类通过在AuthorizeHubConnection()方法中具有以下代码行来使用。

var jwtFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint));
var authenticationTicket = jwtFormat.Unprotect(userJwtToken); 

作为此授权配置的最后一部分,我继承了OpenIdConnectCachingSecurityTokenProvider.cs类中的IIssureSecurityKeyProvider。在下面的代码中可以看到它的完整实现。

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security.Jwt;
//using System.IdentityModel.Tokens;


namespace TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration
{
    //IIssuerSecurityKeyProvider Interface Provides security Key information to the implementing class.

    // This class is necessary because the OAuthBearer Middleware does not leverage
    // the OpenID Connect metadata endpoint exposed by the STS by default.

    internal class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityKeyProvider
    {
        //Manages the retrieval of Configuration data.
        public ConfigurationManager<OpenIdConnectConfiguration> _configManager;

        private string _issuer;
        private IEnumerable<SecurityKey> _keys;

        //this class will be responsible for communicating with the “Metadata Discovery Endpoint” and issue HTTP requests to get the signing keys
        //that our API will use to validate signatures from our IdP, those keys exists in the jwks_uri which can read from the discovery endpoint
        private readonly string _metadataEndpoint;

        //Represents a lock that is used to manage access to a resource, allowing multiple threads for reading or exclusive access for writing.
        private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();
        public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)
        {
            _metadataEndpoint = metadataEndpoint;
            //_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());
            _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());
            //_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);
            RetrieveMetadata();
        }

        /// <summary>
        /// Gets the issuer the credentials are for.
        /// </summary>
        /// <value>
        /// The issuer the credentials are for.
        /// </value>
        public string Issuer
        {
            get
            {
                RetrieveMetadata();
                _synclock.EnterReadLock();
                try
                {
                    return _issuer;
                }
                finally
                {
                    _synclock.ExitReadLock();
                }
            }
        }
        /// <summary>
        /// Gets all known security keys.
        /// </summary>
        /// <value>
        /// All known security keys.
        /// </value>
        public IEnumerable<SecurityKey> SecurityKeys
        {
            get
            {
                RetrieveMetadata();
                _synclock.EnterReadLock();
                try
                {
                    return _keys;
                }
                finally
                {
                    _synclock.ExitReadLock();
                }
            }
        }

        private void RetrieveMetadata()
        {
            _synclock.EnterWriteLock();
            try
            {
                //Task represents an asynchronous operation.
                //Task.Run Method Queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work.
                OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result;
                _issuer = config.Issuer;
                _keys = config.SigningKeys;
            }
            finally
            {
                _synclock.ExitWriteLock();
            }
        }
    }
}

实现此功能后,我们无需在任何集线器方法中都具有[Authorize]属性,并且该中间件将处理请求授权,并且仅授权用户将具有signalR连接,并且只有授权用户才能调用hub方法。 / p>

最后,我想提一下,要使此客户端服务器体系结构正常工作,我们需要有单独的b2c租户客户端应用程序和b2c租户服务器应用程序,并且b2c租户客户端应用程序应具有对b2c租户服务器应用程序的API访问权限。应该按照本示例https://docs.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c-webapi?view=aspnetcore-2.1

那样配置Azure b2c应用程序

尽管它适用于.net核心,但也适用于asp.net,唯一的区别是b2c配置应位于web.config