I am trying to move from a WebAPI based REST service, to one encompassing the new implimentation of OData. I have the service working correctly, but am at a loss on how create unit tests that will test the odata query options.
when unit testing WebAPI methods, I am used to building the httpRequestMessage and injecting it in the constructure:
var request = new HttpRequestMessage();
request.Headers.Add("UserName", "TestUser");
request.Headers.Add("Password", password);
request.Headers.Add("OverRideToken", "false");
request.Headers.Add("AccessSystem", "Mobile");
request.Headers.Add("Seed", "testSeed");
var token = new Token();
var authController = new AuthorizationController(request);
try
{
var returnValue = authController.Get();
how would I go about injecting the odata request? I need to verify that $filter, $inlinecount, and other options are returning the proper records.
You can either test your controller or you can test against a running instance of your Web API (you should probably do both).
Testing your controller won't achieve what you are trying to do, so you will want to test by creating a self hosted in-memory instance of your Web API application. You can then either use HttpClient in your test classes (you will have to manually construct OData requests), or you can use the WCF Data Services Client in your test classes (this will allow you to query via LINQ).
Here's an example using WCF Data Services Client:
public class ODataContainerFactory
{
static HttpSelfHostServer server;
public static MyApplicationServer.Acceptance.ODataService.Container Create(Uri baseAddress)
{
var config = new HttpSelfHostConfiguration(baseAddress);
// Remove self host requirement to run with Adminprivileges
config.HostNameComparisonMode = System.ServiceModel.HostNameComparisonMode.Exact;
// Register Web API and OData Configuration
WebApiConfig.Register(config);
// Configure IoC
ConfigureIoC(dataSource, config);
// Do whatever else, e.g. setup fake data sources etc.
...
// Start server
server = new HttpSelfHostServer(config);
server.OpenAsync().Wait();
// Create container
var container = new MyApplicationServer.Acceptance.ODataService.Container(new Uri(baseAddress.ToString() + "odata/"));
// Configure container
container.IgnoreResourceNotFoundException = true;
container.IgnoreMissingProperties = true;
return container;
}
private static void ConfigureIoC(MockDatasource dataSource, HttpSelfHostConfiguration config)
{
var container = new UnityContainer();
container.RegisterType<TypeA, TypeB>();
...
...
config.DependencyResolver = new IoCContainer(container);
}
public static void Destroy()
{
server.CloseAsync().Wait();
server.Dispose();
}
}
The key here is the WebApiConfig.Register(HttpConfiguration config) method call, which is calling your Web API project.
Note that prior to the above you will need:
Fire up your Web API project
In your test class add a Service Reference to your OData root path.
This will create a Container object (in the example above MyApplicationServer.Acceptance.ODataService.Container), which you can use to query your OData feed in your tests as follows:
var odataContainer = ODataContainerFactory.Create(new Uri("http://localhost:19194/");
var result = odataContainer.MyEntities
.Expand(s => s.ChildReferenceType)
.Where(s => s.EntityKey == someValue).SingleOrDefault();
Related
I'm trying to consume my asp.net web api in my asp.net core mvc web app which are on the same solution. I configured the solution for multi-project start and they start both.
next I tried to consume the API in the Web part but I'm getting the following error.
InvalidOperationException: A suitable constructor for type 'ProjectName.Web.Services.Interfaces.IAdminService' could not be located. Ensure the type is concrete and all parameters of a public constructor are either registered as services or passed as arguments. Also ensure no extraneous arguments are provided.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType, Type[] argumentTypes, out ConstructorInfo matchingConstructor, out Nullable[] matchingParameterMap)
Here is the complete Stack trace
The Projects are structure like this
SolutionName:
Name.API
Name.Web
each with its own respective structure
This is my Helper Class
public static class HttpClientExtensions
{
public static async Task<T> ReadContentAsync<T>(this HttpResponseMessage response)
{
//if (response.IsSuccessStatusCode == false) return StatusCodes = 300;
//throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}");
var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var result = JsonSerializer.Deserialize<T>(
dataAsString, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return result;
}
}
The IAdmin Inter Face
Task<IEnumerable<Admins>> GetAllAdmins();
The AdminService(Implementation)
private readonly HttpClient _client;
public const string BasePath = "api/Admins";
public AdminService(HttpClient client)
{
_client = client; // ?? throw new ArgumentNullException(nameof(client));
}
public async Task<IEnumerable<Admins>> GetAllAdmins()
{
var response = await _client.GetAsync(BasePath);
return await response.ReadContentAsync<List<Admins>>();
}
Admins Controller
private readonly IAdminService _adminService;
public AdminController(IAdminService adminService)
{
_adminService = adminService;
}
public async Task<IActionResult> Index()
{
var adminsList = await _adminService.GetAllAdmins();
if(adminsList == null)
{
return new JsonResult("There are now Admins");
}
return View(adminsList);
}
Program.cs
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient<IAdminService, IAdminService>(c =>
c.BaseAddress = new Uri("https://localhost:<port-Num>/"));
var app = builder.Build();
What Could I be doing wrong???
I'm using .NET 6 adn both Projects are in the same solution
NB My end points are working fine, I test them using Postman.
It is failing because DI cannot instantiate your AdminService with parameterized constructor. This is possibly a duplicate of Combining DI with constructor parameters? .
Essentially, you should avoid parameterized constructor injection where possible. Either control it through configuration or have the configuration loaded through common infrastructure such as host configuration.
According to your codes, I found you put two interface inside the AddHttpClient method which caused the issue.
I suggest you could modify it like this and then it will work well.
builder.Services.AddHttpClient<IAdminService, AdminService>(c =>
c.BaseAddress = new Uri("https://localhost:3333/"));
I am hosting my standalone Blazor WASM app in an Azure Storage Account Static website and wondering how to handle switching between development and production API endpoints using settings in appsettings.json/appsettings.staging.json. The documentation I've found talks more about App Service hosted apps.
I cannot get this Blazor.start() method to work.
I must admit I haven't tried the option to inject an IConfiguration and use HttpClient but would like to check if there's a simple method.
I can't claim to have come up with this myself but I cannot find my reference right now. Anyway I ended up adding this method to my Program.cs.
Program.cs
private static async Task ConfigureApiEndpoint(WebAssemblyHostBuilder builder)
{
var http = new HttpClient()
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};
string apiEndpoint = String.Empty;
if (builder.HostEnvironment.BaseAddress.Contains("localhost"))
apiEndpoint = "api-endpoint-staging.json";
else if (builder.HostEnvironment.BaseAddress.Contains("mysubdomain.domain.com"))
apiEndpoint = "api-endpoint-staging.json";
else
apiEndpoint = "api-endpoint-production.json";
using var response = await http.GetAsync(apiEndpoint);
using var stream = await response.Content.ReadAsStreamAsync();
builder.Configuration.AddJsonStream(stream);
}
api-endpoint-staging.json
Put this in wwwroot folder. Set as Content build action. Do not copy to output.
{
"SirenApi": {
"BaseAddress": "https://mysubdomain.domain.com/api/"
}
}
In our current on-prem setup we have 20+ .net core 3.1 API apps (separate ASP.NET Core API Apps). We have started migrating 2 APi app to Azure App Service tagged with a single Application Insights instance.
In On-Prem, we use some other log framework which the rest of the 18 Apps. All these API apps talk to each other and all the logs are tied to some unique_id in on-prem.
Now, for the apis which is in Azure, we need to leverage the same unique_Id and co-relate everything.
In order to achieve it, I started exploring the functionality of setting a same Operation Id for the 2 apps which are hosted in azure.
Created TelemetrInitializer in both the APIs. and if i set Operational Id as shown below in both the APIs, it's works. All the logs are tied to Single Operation Id "12345"
telemetry.Context.Operation.Id = "12345";
However, as it is obvious to make the Operation Id to be dynamic, I have changed it to the below in my First API
telemetry.Context.Operation.Id = "CR" + Guid.NewGuid().ToString();
So, the next challenge is, I need to tie this new Operation Id in my second API's TelemetryInitiializer. In order to achieve that I tried to grab the Request-Id Header in the TelemetryInitializer of 2nd API. It's always NULL.
Is there a way to achieve this?
Thanks,
Praveen Sreeram.
tldr: This is possible by disabling the built in dependency tracking in .NET Core and App Insights and handling it on your own. In most cases, the best thing to do is let .NET Core and App Insights do the tracking.
I uploaded a simple WebAPI app with the code I'm going to go over to Github: https://github.com/SamaraSoucy-MSFT/customoperationid
There are two things that need to be overridden to get both the headers and App Insights to get the custom operation Id. The first is the Activity the wraps the HttpClient as that controls the correlation headers. The second is the dependency tracing in App Insights.
It is possible to disable Actions completely in your HttpClients, but to minimize side effects, you can just remove the one in the client by setting Activity.Current = null
var operationId = "CR" + Guid.NewGuid().ToString();
var url = "https://www.microsoft.com";
using (var client = new HttpClient())
{
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
//Makes the headers configurable
Activity.Current = null;
//set correlation header manually
requestMessage.Headers.Add("Request-Id", operationId);
await client.SendAsync(requestMessage);
}
}
The next step is to remove the App Insights default tracking for this request. Again, you can disable dependency tracking completely, or you can filter out the default telemetry for this request. Processors are registered inside the Startup class just like initializers.
services.AddApplicationInsightsTelemetryProcessor<CustomFilter>();
public class CustomFilter : ITelemetryProcessor
{
private ITelemetryProcessor Next { get; set; }
// next will point to the next TelemetryProcessor in the chain.
public CustomFilter(ITelemetryProcessor next)
{
this.Next = next;
}
public void Process(ITelemetry item)
{
// To filter out an item, return without calling the next processor.
if (!OKtoSend(item)) { return; }
this.Next.Process(item);
}
// Example: replace with your own criteria.
private bool OKtoSend(ITelemetry item)
{
var dependency = item as DependencyTelemetry;
if (dependency == null) return true;
if (dependency.Type == "Http"
&& dependency.Data.Contains("microsoft.com")
//This key is just there to help identify the custom tracking
&& !dependency.Context.GlobalProperties.ContainsKey("keep"))
{
return false;
}
return true;
}
}
Finally, in the method that makes the remote call, you need to inject a telemetry client and call TelemetryClient.TrackDependency()
var operationId = "CR" + Guid.NewGuid().ToString();
//setup telemetry client
telemetry.Context.Operation.Id = operationId;
if (!telemetry.Context.GlobalProperties.ContainsKey("keep"))
{
telemetry.Context.GlobalProperties.Add("keep", "true");
}
var startTime = DateTime.UtcNow;
var timer = System.Diagnostics.Stopwatch.StartNew();
//continue setting up context if needed
var url = "https:microsoft.com";
using (var client = new HttpClient())
{
//Makes the headers configurable
Activity.Current = null;
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, url))
{
//Makes the headers configurable
Activity.Current = null;
//set header manually
requestMessage.Headers.Add("Request-Id", operationId);
await client.SendAsync(requestMessage);
}
}
//send custom telemetry
telemetry.TrackDependency("Http", url, "myCall", startTime, timer.Elapsed, true);
I am trying to create microservices using Spring-boot Java and SteelToe ASP.NET
Step-1: I created a full service using Java (A service with UI and API. It is hosted on PCF). The API has ClassesControler defined inside.
Step-2: Create a microservice using ASP.NET, SteelToe. Register the service in Eureka and make it discoverable using Zuul.
Step-3: Use the Interface, Service approach to access the JAVA microservice(s)
namespace employee-client.Service
{
public interface IRelayService
{
Task<HttpResponseMessage> getClassesList(string relativeUrl = "/api/v1/classes");
}
}
Service with Implementation for Interface:
namespace employee-client.Service
{
public class RelayService : IRelayService
{
DiscoveryHttpClientHandler _handler;
string _accessToken;
private const string BASE_URL = "https://www.example.com";
public QortaService(IDiscoveryClient client, string accessToken)
{
_handler = new DiscoveryHttpClientHandler(client);
_accessToken = accessToken;
}
public async Task<HttpResponseMessage> getClassesList(string relativeUrl)
{
string classesUrl= BASE_URL + relativeUrl;
HttpClient client = GetClient();
HttpRequestMessage request = new HttpRequestMessage();
request.RequestUri = new Uri(classesUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
return await client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
}
private HttpClient GetClient()
{
var client = new HttpClient(_handler, false);
return client;
}
}
}
I came up with this approach based on the example in SteelToe but I hate hardcoding the BASE_URL.
Question: I very much like the #FeignClient annotation approach used in Java. Any ideas about how I can access an existing microservice in a better way. If so, an example would be much appreciated
Edit:
I modified the question to make more clear.
The flow of traffic is from Java Service to .NET service. .NET service requests for a list of classes from the controller in JAVA service (ClassesController.java)
I'm unclear which direction traffic is flowing in your scenario, but I think you're saying the .NET application is trying to call the Java application. The code you're using is from before HttpClientFactory was introduced and is a bit clunkier than what's possible now in general. Steeltoe can be used with HttpClientFactory for a better overall experience.
Steeltoe has debug logging available to confirm the results of service lookup if you set logging:loglevel:Steeltoe.Common.Discovery = true in your application config.
You didn't mention specifically what isn't working, but I'm guessing you're getting a 404 since it looks like your code will create a request path looking like https://fortuneService/api/fortunes/random/api/v1/classes
If you're looking for something like Feign in .NET, you could try out DHaven.Faux
For others who are looking for the same:
namespace employee-client.Service
{
public class RelayService : IRelayService
{
private const string CLASSES_API_SERVICEID = "classes-api";
IDiscoveryClient _discoveryClient;
DiscoveryHttpClientHandler _handler;
string _accessToken;
public RelayService(IDiscoveryClient discoveryClient, string accessToken)
{
_discoveryClient = discoveryClient;
_handler = new DiscoveryHttpClientHandler(client);
_accessToken = accessToken;
}
public async Task<HttpResponseMessage> getClassesList()
{
var classesApiInstances = _discoveryClient.GetInstances(CLASSES_API_SERVICEID);
Uri classesApiUri = classesApiInstances[0].Uri;
string classesUrl= classesApiUri.AbsoluteUri + relativeUrl;
HttpClient httpClient = GetClient();
HttpRequestMessage request = new HttpRequestMessage();
request.RequestUri = new Uri(classesUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
return await httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);
}
private HttpClient GetClient()
{
var client = new HttpClient(_handler, false);
return client;
}
}
}
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.
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://learn.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