Azure Pubsub Events on Server - c#

I'm trying to get an Azure tutorial working with pubsub. I can't get the server endpoint that handles the calls from the pubsub service to work.
This is the code for Program.cs to wireup the end points:
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/negotiate", async (WebPubSubServiceClient<Sample_ChatApp> serviceClient, HttpContext context) =>
{
var id = context.Request.Query["id"];
if (id.Count != 1)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("missing user id");
return;
}
await context.Response.WriteAsync(serviceClient.GetClientAccessUri(userId: id).AbsoluteUri);
});
endpoints.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");
endpoints.MapGet("/msg", async (WebPubSubServiceClient<Sample_ChatApp> serviceClient, HttpContext context) =>
{
await serviceClient.SendToAllAsync("test");
});
});
When hitting the endpoint I get the following exception:
An unhandled exception occurred while processing the request.
InvalidOperationException: The request reached the end of the pipeline without executing the endpoint: '/eventhandler'. Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if using routing.
Microsoft.AspNetCore.Builder.ApplicationBuilder+c.b__18_0(HttpContext context)
Stack Query Cookies Headers Routing
InvalidOperationException: The request reached the end of the pipeline without executing the endpoint: '/eventhandler'. Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if using routing.
Microsoft.AspNetCore.Builder.ApplicationBuilder+c.b__18_0(HttpContext context)
Microsoft.Azure.WebPubSub.AspNetCore.WebPubSubMiddleware.InvokeAsync(HttpContext context)
Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Show raw exception details
I have to be missing something silly but I've been looking at this for hours and I've got no idea. I assumed there was a step I missed so I downloaded the demo but I get the same result with that version of the code.

I have double checked this issue, and please allow me to show you show to run this sample perfectly. The issue in your post, is missing some settings about the eventhandler in your azure portal.
Test Result
Steps
Download the sample project and open it.
Add the code like you about the "/msg".
Using ngrok and generate an accessable url. Command :
ngrok http https://localhost:5001
Check the generated url, is it working well
Add eventhandler in azure portal by using the generated url.
Test result pic

Related

Custom middleware exception handler response content not passed

I'm having a strange problem with ASP.Net Core custom middleware.
I've got an exception handler that I'm trying to log exceptions and then send a generic message to the caller without the exception details that seem to be sent by default.
The problem is that the response is being sent without a body.
My code:
public async Task Invoke(HttpContext context)
{
try
{
await next.Invoke(context);
}
catch (Exception e)
{
logger.LogError(e, "Request threw an exception");
context.Response.StatusCode = 500;
using (var writer = new StreamWriter(context.Response.Body))
{
await writer.WriteAsync($"Error on server processing request");
await writer.FlushAsync();
}
}
}
I'm using .Net 5. This project is a Web API project, in case that makes any difference.
I'm invoking my API via swagger UI and I'm also checking the responses using the dev tools in Edge (Chromium version) to make sure that swagger isn't hiding the body.
It appears that there is no body to the response, it's just an empty response with a 500 code.
Running in the debugger shows that it is executing the code and if I change the response code to 566 that response code is received by SwaggerUI so it's doing something.
Note: before I added the FlushAsync() call, I was getting an exception sent to Swagger saying that the dispose was using a synchronous write when flushing so that seems to be necessary.
Update:
Pipeline configuration, as requested:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment() || env.IsTesting())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Clients v1"));
}
app.UseRequestLoggingMiddleware(); // My custom middleware
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Behaviour is the same regardless of the environment (development, testing, staging or production)
I'm uncertain why using context.Response.Body is not working, but what I've done instead is use the methods directly on context.Response:
context.Response.StatusCode = 500;
await context.Response.WriteAsync($"Error on server processing request");
Update: Given your middleware pipeline starts with app.UseDeveloperExceptionPage() I suspect that's what's overwriting the response - at least if you're running with dev or test as environment. This is the middleware that actually exposes the full exception details you are saying you're trying to avoid. In the ASP.NET Core project boilerplate/template this is intentionally only added when not running on a production environment.
So perhaps your problem will be solved by changing the ASPNETCORE_ENVIRONMENT environment variable to something other than Development or Test. Or if you still want your own middleware, you should probably remove app.UseDeveloperExceptionPage() and perhaps even move your own app.UseRequestLoggingMiddleware() up as the first line in Configure (although I don't think the Swagger stuff should interfere - but I make no promises :) )
I found the problem. The code I needed was
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync($"Error on server processing request");
I noticed that if I requested the API by typing the url into the web browser, I was getting the text back but when I requested via Swagger UI, it was not sending the text.
SwaggerUI was setting an accept: text/plain header with the request so ASP.Net was ignoring any content that wasn't set as this type (the ContentType is null by default).
The browser had */* in its accept header so the content was being sent regardless of type.

MsalUiRequiredException when calling Microsoft Graph SDK from NET Core web app

We have a NET Core 3.1 web application where users are authenticated in Azure AD with the Microsoft.Identity.Web package. We are calling the Microsoft Graph SDK on behalf of the signed in users as described here.
After logging in to the application everything works fine - all calls to Microsoft Graph SDK succeed. However, after about 30-60 minutes (we haven't timed it exactly) users remain authenticated in our application but calls to the Microsoft Graph SDK fail with the following error:
IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user.
And the inner exception has:
Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
Users then need to log out of the application and log back in, after which calls to MS Graph succeed again for 30-60 minutes.
Can anyone shed any light on this?
Our Setup
In appsettings.json :
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<our-domain-name>",
"TenantId": "<our-tenant-id>",
"ClientId": "<our-client-id>",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
"ClientSecret": "<secret>"
},
"GraphBeta": {
"BaseUrl": "https://graph.microsoft.com/beta",
"Scopes": "User.Read Sites.Read.All Files.Read.All Sites.ReadWrite.All Files.ReadWrite.All",
"DefaultScope": "https://graph.microsoft.com/.default"
}
In Startup.cs :
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp( Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "User.Read","Sites.Read.All","Files.Read.All","Sites.ReadWrite.All","Files.ReadWrite.All" })
.AddMicrosoftGraph(Configuration.GetSection("GraphBeta"))
.AddDistributedTokenCaches();
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("AzureConnection");
options.SchemaName = "dbo";
options.TableName = "TokenCache";
});
The GraphServiceClient is injected into our controllers :
readonly ITokenAcquisition tokenAcquisition;
private readonly GraphServiceClient graphServiceClient;
public SearchController(IConfiguration configuration, ITokenAcquisition _tokenAcquisition,
GraphServiceClient _graphServiceClient) : base(configuration)
{
tokenAcquisition = _tokenAcquisition;
graphServiceClient = _graphServiceClient;
}
And the relevant actions decorated with AuthorizeForScopes :
[AuthorizeForScopes(ScopeKeySection = "GraphBeta:Scopes")]
public async Task<IActionResult> Results(string queryString, int pageNumber, int pageSize)
{
...
}
And then calls are made to MS Graph, for example:
return await graphClient.Search.Query(requests).Request().PostAsync();
We are using
Microsoft.Identity.Web 1.4.1
Microsoft.Identity.Web.MicrosoftGraphBeta 1.4.1
Microsoft.Graph.Beta 0.35.0-preview
Microsoft.Graph.Auth 1.0.0-preview.6
Adding a solution/workaround here - I was getting this error only when debugging in VS, because the cookie would persist but not the server-side session state. So rapidly debugging the web application was causing this exception.
You can work around it by adding some exception-handling middleware that just clears the login cookie (which will re-execute the interactive login flow).
You could optionally wrap this in a development enviornment condition (env.IsDevelopment()), but if this error were to happen in production, it will handle that gracefully as well.
// Program.cs (C# 10, .NET 6)
app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = async ctx => {
var feature = ctx.Features.Get<IExceptionHandlerFeature>();
if (feature?.Error is MsalUiRequiredException
or { InnerException: MsalUiRequiredException }
or { InnerException.InnerException: MsalUiRequiredException })
{
ctx.Response.Cookies.Delete($"{CookieAuthenticationDefaults.CookiePrefix}{CookieAuthenticationDefaults.AuthenticationScheme}");
ctx.Response.Redirect(ctx.Request.GetEncodedPathAndQuery());
}
}
});
It would appear that for us this has been solved my enabling Multi-Factor Authentication on our tenant.
Users originally just used a username and password to log into their Azure AD accounts, but since enabling MFA the above errors have stopped occurring.
Unfortunately we can only speculate as to the reasons why, so we can't provide an explanation for why this solves the issue.
Not sure if you're still experiencing this issue, but I had this exact problem and found the AuthorizeForScopes attribute needs the exact scopes that I am using in that method. Any incorrect scopes there will result in a challenge, which means that MsalUiRequiredException gets thrown.
Try hardcoding the parameters in that attribute to whatever scopes are required here in your code:
return await graphClient.Search.Query(requests).Request().PostAsync();
I did this on my end and am no longer seeing the issue even after leaving the app for a few hours (without refreshing in the hopes that the token expires).
For example, I have a method here that uploads some files to a Shared Document Library, and these are my scopes (which appear to be working):
[AuthorizeForScopes(Scopes = new[] { "MyFiles.Read", "MyFiles.Write", "Sites.Search.All" })]
public async Task<IActionResult> Upload(IFormFile file)
Lastly, if there are still issues, go to your App Registration in Azure > Manifest > knownClientApplications and add your Client ID into it. I actually don't know if this does anything to solving the problem, but I did it before I figured out the scope stuff, and found it began to work intermittently.
Posting here in the hope I might save someone some time. If your application is an MVC application in the traditional server-side rendering style, then dealing with this (expected) behaviour is a two step process:
Add the [AuthorizeForScopes] attribute to your controller(s) and make sure it includes the scopes you need authorisation for. For me that was https://apps.azureiotcentral.com/user_impersonation. For those calling Microsoft Graph, it'll be user.read. NOTE: You can also use this attribute in the form [AuthorizeForScopes(ScopeKeySection = "Path:to:scopes:in:appsettings")]. AuthorizeForScopes is an exception filter that doesn't do anything unless it sees exceptions telling it that the user needs to log in again. When it catches an exception containing a MsalUiRequiredException it will then send the user off to login.microsoftonline.com (or your corporate AAD tenant) to log in again. Generally this won't actually produce a login form as various cookie-based magic will silently happen, and the user will then be quietly redirected back to your page where a repeat request to your controller will succeed.
Make sure that you allow Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException exceptions to bubble all the way up and out of your controller actions. In my case, I'd added exception handling code around my calls to ITokenAquisition.GetAccessTokenForUserAsync(...) that was not allowing these exceptions out, so the AuthorizeForScopes attribute couldn't work its magic.
Exception handling code like this will do the trick:
try
{
// Custom code that ultimately ends up calling
// ITokenAcquisition.GetAccessTokenForUserAsync(...)
accessToken = await this.GetBearerTokenAsync();
}
catch (MicrosoftIdentityWebChallengeUserException)
{
// If a new challenge to the user (i.e. log in again, or apply
// authentication cookie) is required, then the call to GetBearerTokenAsync
// will throw an MsalUiRequiredException inside a
// MicrosoftIdentityWebChallengeUserException. We should re-throw this, so
// it can be caught by the [AuthorizeForScopes] exception handling attribute
// added to our application's base AuthenticatedController class.
throw;
}
catch (Exception ex)
{
// Handle other exceptions gracefully
exceptionMessage = ex.Message;
}

ASP.Net Core MVC Middleware - Get Target URL From Secondary File Requests

I am trying to use custom middleware to intercept and modify requests to a certain controller (FooController) while letting other requests go through as usual. I am trying to identify these using context.Request.Path as shown:
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.Value.StartsWith("/Foo", StringComparison.OrdinalIgnoreCase))
{
// do stuff
}
...
}
The problem is that navigating to https://localhost/Foo/Index creates several actual requests:
/Foo/Index
/js/foo-script.js
/images/my-image.png
I would like to be able to intercept and modify all of these related requests, and my current approach only catches the first request. The closest question I have been able to find is this Current URL in ASPCore Middleware? but the provided extension methods still don't show the URL that the user typed or the link they clicked... only the file that is currently being retrieved. Is there any property in the HttpContext that will show me the "parent request" for the scripts, images, stylesheets, and other assets referenced by the Index view?
EDIT: I can set a breakpoint and see the middleware being invoked, and I can see that /Foo/Index matches the if statement and that /js/foo-script.js does not, so that part seems to be fine. The middleware is registered in startup.cs like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMyMiddleware();
...
}
using the following extension method as a helper (this part is all working as expected):
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.Use(next => new FooMiddleware(next).Invoke);
}
Is there any property in the HttpContext that will show me the "parent request" for the scripts, images, stylesheets, and other assets referenced by the Index view?
Try the "Referer" request header:
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path;
var referer = context.Request.Headers["Referer"];
Console.WriteLine($"Referer: {referer} Path: {path}");
await _next(context);
}
For instance, if I navigate from the /Bar page to the /Foo page, then I see the following output:
Referer: https://localhost:5001/Bar Path: /Foo
Referer: https://localhost:5001/Foo Path: /css/site.css
That second line means that /Foo was the "parent request" for the /css/site.css file.

Abort connection in case of Unauthorized JWT sent in http request to Web API .net Core 2

I am writing a .net Core 2.0 Web API controller that performs file upload using a multipart type http request and is based in the streaming technique described here.
At this point I have to say that I if you want you can skip the next two paragraphs that describe the reason that led me to the need for a solution to the problem that is described after the two paragraphs.
I initially thought of authenticating the user by sending authentication data in the first section of the multipart request and validating the user as soon as the user data are read, by contacting the database and performing the proper request. However, I thought that since this is a streaming request, any delay in authenticating the user using the database, would delay reading the stream with the file. This would cause the TCP receive buffer to fill with data (possibly also increase its size) and would defeat the purpose of streaming the file (instead of buffering), since memory consumption for this connection would increase.
In order to get rid of this issue I thought of using a 2 step authentication using JWTs. The Web API user will first perform a request and ask for a JWT. Then it would use this JWT in the upload request. As I understand it, JWT authentication should be much faster than a database request since it is performed by validating the JWT using the key stored in the server, so the previous issue should not exist.
I implemented the JWT authentication for the upload request following this very good description from Auth0 and it worked just fine. More specifically the controller has an [Authorize] attribute that forces Web API to to authenticate the user by validating the JWT before the controller is executed.
The problem I am facing is that with the above proposed solution when an unauthorized user tries to upload a file the Controller action is never called. The Authentication engine returns an Unathorized (401) response to the user and lets the user continue sending file data. The last part is my problem. I would like unauthorized users, which are probably attackers, to receive the 401 response and then have their connection terminated.
So, what I want is to keep the authentication/authorization part as it already works and also terminate the user connection after sending the 401 response. I know (and have also tested it) that from inside a controller action method an http connection can be terminated by calling
HttpContext.Abort();
I suspect that by using a filter, I could do what I want but I am not very familiar with filters so that is why I am asking.
We can achieve that by using an IAuthorizationFilter.
Inside it, we gonna set an special ActionResult called AbortUnauthorizedConnectionResult and in that we set the Status Code to 401 and Content-Length to 0 and by calling Response.Body.Flush() we make sure it's sent to client before we call Abort().
Here we have an AuthorizationFilter called AbortUnauthorizedConnections:
class AbortUnauthorizedConnections : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User?.Identity == null || !context.HttpContext.User.Identity.IsAuthenticated)
{
// by setting this we make sure the pipe-line will get short-circuited.
context.Result = new AbortUnauthorizedConnectionResult();
}
}
}
And because we have inherited from Attribute we can use it on the upload action like this:
[Authorize]
[AbortUnauthorizedConnections]
public async Task<IActionResult> UploadFile()
{
// we do whatever we want.
}
Here is the code for AbortUnauthorizedConnectionResult:
class AbortUnauthorizedConnectionResult : StatusCodeResult
{
public AbortUnauthorizedConnectionResult() : base(401)
{
}
public override async Task ExecuteResultAsync(ActionContext context)
{
await base.ExecuteResultAsync(context);
context.HttpContext.Response.Headers.Add("Content-Length", "0");
context.HttpContext.Response.Body.Flush();
context.HttpContext.Abort();
}
}
Now if an unauthorized user try to access this controller will get 401 and it's connection gets aborted.
This is the solution I actually implemented due to its simplicity, following #Tratcher's advice:
First, I deleted the [Authorize] attribute from my Controller Action method. Then I wrote the beginning of my Controller Action method as follows:
public async Task<string> UploadFile()
{
if (!(await HttpContext.AuthenticateAsync()).Succeeded)
{
HttpContext.Response.StatusCode = 401; //Unauthorized
HttpContext.Response.Headers.Add("Content-Length", "0");
HttpContext.Response.Body.Flush();
HttpContext.Abort();
return null;
}
...
}

SignalR 404 error when trying to negotiate

So I am very new to SignalR, in fact I've only been using it for a couple of days now. Anyway, I am getting the error below when my application first starts up:
The code for the application in question is located in two projects, a Web API and a Single Page Application (SPA). The first one has my backend code (C#) and the second one my client-side code (AngularJS). I think the problem might be due to the fact that the projects in question run on different ports. The Web API, where my SignalR hub lives, is on port 60161 and the SPA is on 60813. My hub is declared like so:
public class ReportHub : Hub
{
public void SendReportProgress(IList<ReportProgress> reportProgress)
{
this.Clients.All.broadcastReportProgress(reportProgress);
}
public override Task OnConnected()
{
this.Clients.All.newConnection();
return base.OnConnected();
}
}
and then in my Startup.cs file for my Web API I initialize SignalR like this:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.Services.Replace(typeof(IHttpControllerActivator), new NinjectFactory());
config.MessageHandlers.Add(new MessageHandler());
//set up OAuth and Cors
this.ConfigureOAuth(app);
config.EnableCors();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
// Setting up SignalR
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
map.RunSignalR(new HubConfiguration { EnableJSONP = true });
});
//set up json formatters
FormatterConfig.RegisterFormatters(config.Formatters);
WebApiConfig.Register(config);
app.UseWebApi(config);
}
For my client-side code I use an Angular SignalR API called angular-signalr-hub (Angular-signalr-hub). The client-side follows:
angular
.module("mainApp")
.factory("reportHubService", ["$rootScope", "Hub", reportHubService]);
/// The factory function
function reportHubService($rootScope, Hub) {
var vm = this;
vm.reportName = "None";
// Setting up the SignalR hub
var hub = new Hub("reportHub", {
listeners: {
'newConnection': function(id) {
vm.reportName = "SignalR connected!";
$rootScope.$apply();
},
'broadcastReportProgress': function (reportProgress) {
vm.reportName = reportProgress.reportName;
$rootScope.$apply();
}
},
errorHandler: function(error) {
},
hubDisconnected: function () {
if (hub.connection.lastError) {
hub.connection.start();
}
},
transport: 'webSockets',
logging: true
//rootPath: 'http://localhost:60161/signalr'
});
I did some googling yesterday and one of the suggestions I came upon was to set the SignalR URL to the one of my Web API, which I did (the commented out line above). When I uncomment the line in question, that does seem to do something because if I now go to http://localhost:60161/signalr/hubs in my browser, it does show me the dynamically generated proxy file:
and when I run my application I no longer get the error above, but now it doesn't seem to connect. It gets to the negotiate line and it stops there:
I think it should look like this (this is from a SignalR tutorial I found):
In addition, none of my listeners (declared in my Angular code above) get called, so something is still now working quite right. There should be more lines in the log to the effect that connection was successfully established, etc. What could be the problem here?
UPDATE: upon further debugging i found out the problem is most likely being caused by the ProtocolVersion property being different between the client and the result here:
Because of that it seems it just exists and fails to establish connection.
I figured out what the problem was. My SignalR dependencies were out of date and because of that my client and server versions differed. All I had to do was update (via NuGet Package Manager) all SignalR dependencies to the latest version and now it works.
As a side note, SignalR was not very good at telling me what was wrong. In fact, no error message was displayed, unless of course there was some additional logging somewhere that had to be found or turned on, in addition to the logging I already had (turned on). Either way, it's either not logging certain errors or it makes it difficult to figure out how to turn on all logging. I had to go and debug the JQuery SignalR api to figure out what the problem was, which was a time consuming endeavour.

Categories

Resources