din*_*ota 5 asp.net signalr azure-ad-b2c
我有两个应用程序……一个是 JavaScript signalR 客户端,另一个是 asp.net Web 应用程序,用作 signalR 服务器以向客户端广播更新。我尝试使用 azure Active Directory B2C 服务通过客户端应用程序为用户提供身份验证和授权,以访问服务器中的资源。因此,只有经过身份验证的 JavaScript 客户端用户才能在令牌验证后启动与托管 signalR 服务器的 ASP.NET Web 应用程序的 signalR 连接。由于 signalR 使用 Web 套接字,我们无法在 HTTP 连接请求标头中提供令牌。看来我应该使用查询字符串在 signalR 连接请求中提供身份验证令牌。在 asp.net 服务器应用程序中收到该令牌后,我需要验证该令牌并允许 JavaScript 客户端应用程序具有 signalR 连接。我想在这篇博客文章https://kwilson.io/blog/authorize-your-azure-ad-users-with-signalr/中实现完全相同的事情,但使用 azure Active Directory b2c。
似乎其他人使用 ASP.NET SignalR 客户端和服务器架构也可能遇到同样的问题。\n实际上,经过大量的努力,我能够通过自定义signalR集线器的AuthorizeModule来解决这个问题。实际上,我使用 CustomAuthorization 类中的 AuthorizeAttribute 继承来重写 AuthorizeHubConnection() 和 AuthorizeHubMethodInspiration() 。\n首先,我在启动配置中的app.Map("/signalr", map =>{ .... } 中添加了GlobalHost.HubPipeline.AddModule(module)。您可以在下面的startup.cs中看到它。
\n\nusing Microsoft.Owin;\nusing Microsoft.Owin.Cors;\nusing Owin;\nusing Microsoft.AspNet.SignalR;\nusing TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration;\nusing Microsoft.AspNet.SignalR.Hubs;\n\n[assembly: OwinStartup(typeof(TestCarSurveillance.RealTimeCommunication.Startup))]\n\nnamespace TestCarSurveillance.RealTimeCommunication\n{\n public class Startup\n {\n public void Configuration(IAppBuilder app)\n {\n //After adding Authorization module in GlobalHost.HubPipeline.AddModule(module)\n //program was unable to create the log file so I have added it.\n log4net.Config.XmlConfigurator.Configure();\n\n // Branch the pipeline here for requests that start with "/signalr"\n //app.UseWelcomePage("/");\n app.Map("/signalr", map =>\n {\n // Setup the CORS middleware to run before SignalR.\n // By default this will allow all origins. You can \n // configure the set of origins and/or http verbs by\n // providing a cors options with a different policy.\n\n map.UseCors(CorsOptions.AllowAll);\n var hubConfiguration = new HubConfiguration\n {\n EnableDetailedErrors = true,\n // You can enable JSONP by uncommenting line below.\n // JSONP requests are insecure but some older browsers (and some\n // versions of IE) require JSONP to work cross domain\n EnableJSONP = true\n };\n\n // Require authentication for all hubs\n var authorizer = new CustomAuthorization();\n var module = new AuthorizeModule(authorizer, authorizer);\n GlobalHost.HubPipeline.AddModule(module);\n\n map.RunSignalR(hubConfiguration);\n });\n }\n\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n此 Authorize 模块调用每个 signalR hub 中的 CustomAuthorize.cs 类 OnConnected()、OnDisconnected()、OnReconnected() 以及客户端可以调用的 hub 方法。
\n\nusing Microsoft.AspNet.SignalR;\nusing Microsoft.AspNet.SignalR.Hubs;\nusing Microsoft.AspNet.SignalR.Owin;\nusing Microsoft.IdentityModel.Tokens;\nusing Microsoft.Owin.Security.Jwt;\nusing System;\nusing System.Collections.Generic;\nusing System.Configuration;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Security.Claims;\n\nnamespace TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration\n{\n\n public class CustomAuthorization : AuthorizeAttribute\n {\n // These values are pulled from web.config for b2c authorization\n public static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];\n public static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];\n public static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];\n public static string signUpInPolicy = ConfigurationManager.AppSettings["ida:SignUpInPolicyId"];\n\n static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);\n\n //This method is called multiple times before the connection with signalR is established.\n public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)\n {\n var metadataEndpoint = string.Format(aadInstance, tenant, signUpInPolicy);\n // Extract JWT token from query string.\n var userJwtToken = request.QueryString.Get("Authorization");\n if (string.IsNullOrEmpty(userJwtToken))\n {\n return false;\n }\n\n // Validate JWT token.\n //var tokenValidationParameters = new TokenValidationParameters { ValidAudience = ClientId };\n //Contains a set of parameters that are used by a SecurityTokenHandler when validating a SecurityToken.\n TokenValidationParameters tvps = new TokenValidationParameters\n {\n // Accept only those tokens where the audience of the token is equal to the client ID of this app\n // This is where you specify that your API only accepts tokens from its own clients\n // here the valid audience is supplied to check against the token\'s audience\n ValidAudience = clientId,\n ValidateIssuer = false,\n // It is the authentication scheme used for token validation\n AuthenticationType = signUpInPolicy,\n //SaveSigninToken = true,\n\n //I\xe2\x80\x99ve configured the \xe2\x80\x9cNameClaimType\xe2\x80\x9d of the \xe2\x80\x9cTokenValidationParameters\xe2\x80\x9d to use the claim named \xe2\x80\x9cobjectidentifer\xe2\x80\x9d (\xe2\x80\x9coid\xe2\x80\x9d) \n //This will facilitate reading the unique user id for the authenticated user inside the controllers, all we need to call \n //now inside the controller is: \xe2\x80\x9cUser.Identity.Name\xe2\x80\x9d instead of querying the claims collection each time\n\n //Gets or sets a String that defines the NameClaimType.\n NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier"\n };\n try\n {\n var jwtFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint));\n var authenticationTicket = jwtFormat.Unprotect(userJwtToken);\n\n if(authenticationTicket != null && authenticationTicket.Identity !=null && authenticationTicket.Identity.IsAuthenticated)\n {\n var email = authenticationTicket.Identity.FindFirst(p => p.Type == "emails").Value;\n\n // It is done to call the async method from sync method \n //the ArgumentException will be caught as you\xe2\x80\x99d expect, because .GetAwaiter().GetResult() unrolls the first exception the same way await does. \n //This approach follows the principle of least surprise and is easier to understand.\n // set the authenticated user principal into environment so that it can be used in the future\n request.Environment["server.User"] = new ClaimsPrincipal(authenticationTicket.Identity);\n\n return true;\n }\n }\n catch (Exception ex)\n {\n Debug.WriteLine(ex);\n log.Error(ex);\n //throw ex;\n\n }\n\n return false;\n }\n\n public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)\n {\n var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;\n //Check the authenticated user principal from environment\n var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;\n //ClaimsPrincipal supports multiple claims based identities\n var principal = environment["server.User"] as ClaimsPrincipal;\n if(principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)\n {\n // create a new HubCallerContext instance with the principal generated from token\n // and replace the current context so that in hubs we can retrieve current user identity\n hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);\n return true;\n }\n return false; \n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n从查询字符串收到令牌后,我们需要设置 TokenValidationParameters,在metadataEndpoint 中使用它进行令牌验证。令牌验证是在建立集线器连接之前完成的,因此只有授权用户才能建立连接,如果连接不成功,则会返回 401 响应。它在 OpenIdConnectCachingSecurityTokenProvider.cs 类中实现。通过在 AuthorizeHubConnection() 方法中包含以下代码行来使用此类。
\n\nvar jwtFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint));\nvar authenticationTicket = jwtFormat.Unprotect(userJwtToken); \nRun Code Online (Sandbox Code Playgroud)\n\n作为此授权配置的最后一部分,我继承了 OpenIdConnectCachingSecurityTokenProvider.cs 类中的 IIssureSecurityKeyProvider。其完整的实现可以参见下面的代码。
\n\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Microsoft.IdentityModel.Protocols;\nusing Microsoft.IdentityModel.Protocols.OpenIdConnect;\nusing Microsoft.IdentityModel.Tokens;\nusing Microsoft.Owin.Security.Jwt;\n//using System.IdentityModel.Tokens;\n\n\nnamespace TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration\n{\n //IIssuerSecurityKeyProvider Interface Provides security Key information to the implementing class.\n\n // This class is necessary because the OAuthBearer Middleware does not leverage\n // the OpenID Connect metadata endpoint exposed by the STS by default.\n\n internal class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityKeyProvider\n {\n //Manages the retrieval of Configuration data.\n public ConfigurationManager<OpenIdConnectConfiguration> _configManager;\n\n private string _issuer;\n private IEnumerable<SecurityKey> _keys;\n\n //this class will be responsible for communicating with the \xe2\x80\x9cMetadata Discovery Endpoint\xe2\x80\x9d and issue HTTP requests to get the signing keys\n //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\n private readonly string _metadataEndpoint;\n\n //Represents a lock that is used to manage access to a resource, allowing multiple threads for reading or exclusive access for writing.\n private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();\n public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)\n {\n _metadataEndpoint = metadataEndpoint;\n //_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());\n _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());\n //_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);\n RetrieveMetadata();\n }\n\n /// <summary>\n /// Gets the issuer the credentials are for.\n /// </summary>\n /// <value>\n /// The issuer the credentials are for.\n /// </value>\n public string Issuer\n {\n get\n {\n RetrieveMetadata();\n _synclock.EnterReadLock();\n try\n {\n return _issuer;\n }\n finally\n {\n _synclock.ExitReadLock();\n }\n }\n }\n /// <summary>\n /// Gets all known security keys.\n /// </summary>\n /// <value>\n /// All known security keys.\n /// </value>\n public IEnumerable<SecurityKey> SecurityKeys\n {\n get\n {\n RetrieveMetadata();\n _synclock.EnterReadLock();\n try\n {\n return _keys;\n }\n finally\n {\n _synclock.ExitReadLock();\n }\n }\n }\n\n private void RetrieveMetadata()\n {\n _synclock.EnterWriteLock();\n try\n {\n //Task represents an asynchronous operation.\n //Task.Run Method Queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work.\n OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result;\n _issuer = config.Issuer;\n _keys = config.SigningKeys;\n }\n finally\n {\n _synclock.ExitWriteLock();\n }\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n\n实现此功能后,我们不需要在任何集线器方法中具有 [Authorize] 属性,并且该中间件将处理请求授权,并且只有授权用户才会有 signalR 连接,并且只有授权用户才能调用集线器方法。
\n\n最后我想提一下,要使此客户端服务器架构正常工作,我们需要拥有单独的 B2C 租户客户端应用程序和 B2C 租户服务器应用程序,并且 B2C 租户客户端应用程序应该具有对 B2C 租户服务器应用程序的 API 访问权限。Azure b2c 应用程序应按照此示例进行配置https://learn.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c-webapi?view=aspnetcore-2.1
\n\n虽然它适用于.net core,但它也适用于asp.net,唯一的区别是b2c配置应该位于web.config
\n| 归档时间: |
|
| 查看次数: |
2035 次 |
| 最近记录: |