1
votes

I have two application...... one is JavaScript signalR client and the other one is asp.net web application used as signalR server to broadcast the updates to the client. And I was trying to use azure active directory b2c service to offer authentication and authorization for user through client application to access the resources in the server. So, that only the authenticated user of JavaScript client can initiate signalR connection with the asp.net web application hosting signalR server after the token validation. As, signalR uses web-sockets we cannot supply the token in the HTTP connection request header. It seems that I should use query string to supply authentication token in the signalR connection request. After receiving that token in the asp.net server application I need to validate that token and allow the JavaScript client application to have a signalR connection. I want to implement exactly the same thing in this blog post https://kwilson.io/blog/authorize-your-azure-ad-users-with-signalr/ but using azure active directory b2c.

1
Your question is really broad and it's not clear exactly what you're having trouble with. Can you provide an example problem for others to respond to?phalteman
I have modified the question if it helps.dinesh sapkota
I am attempting to do this as well with very little luck.akousmata
After struggling for some time I was able to achieve jwt token validation in signalR server. I am not sure if it is a best solution but it works. And you can check in the following answer in this post.dinesh sapkota

1 Answers

2
votes

It seems like others might also have same problem using ASP.NET SignalR Client and server architecture. Actually, with lots of efforts I was able to solve this issue by customizing the AuthorizeModule of signalR hubs. Actually I override AuthorizeHubConnection() and AuthorizeHubMethodInvocation() using AuthorizeAttribute inheritance in CustomAuthorization class. First of all I added the GlobalHost.HubPipeline.AddModule(module) in app.Map("/signalr", map =>{ .... } in startup Configuration. You can see it in the following 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);
            });
        }

    }
}

This Authorize module calls CustomAuthorize.cs class in each signalR hub OnConnected(), OnDisconnected(), OnReconnected() and hub methods that the client can call.

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

After we receive the token from the query string we need to setup TokenValidationParameters use it in metadataEndpoint for token validation. The token validation is done in before having the hub connection so, that only the authorized user can have a connection and if the connection is not successful it returns 401 response. It is implemented in OpenIdConnectCachingSecurityTokenProvider.cs class. This class is being used by having following line of code in AuthorizeHubConnection() method.

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

As, the last part of this authorization configuration I have inherited IIssureSecurityKeyProvider in OpenIdConnectCachingSecurityTokenProvider.cs class. The complete implementation of it can be seen in the following code.

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

After implementing this we do not need to have [Authorize] attribute in any hub method and this middle-ware will handle the request authorization and only authorized user will have a signalR connection and only authorized user can invoke the hub method.

At last I would like to mention that for this client server architecture to work we need to have separate b2c tenant client application and b2c tenant server application and b2c tenant client application should have API access to the b2c tenant server application. Azure b2c application should be configured as in this example https://docs.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c-webapi?view=aspnetcore-2.1

Although, it is for .net core but it is also valid for asp.net and only difference is that b2c configuration should be at web.config