How to configure custom error pages for exceptions in view files? - asp.net-core

We're using UseStatusCodePagesWithReExecute in combination with a simple custom middleware that sets Response.StatusCode to 500, to successfully to send users to our custom error page on exceptions that occur in mvc controllers.
However, for exceptions that occur in razor/cshtml views, UseStatusCodePagesWithReExecute doesn't send the user to our error page (though our custom middleware does detect these exceptions in Invoke()).
We tried using an Exception Filter as well, but it only traps exceptions from controller actions, not from views.
Is there a way to send users to our error page if the exception originates in a view?

StatusCodePagesMiddleware added by UseStatusCodePagesWithReExecute extension call has the following check after executing of underlying middlewares:
// Do nothing if a response body has already been provided.
if (context.Response.HasStarted
|| context.Response.StatusCode < 400
|| context.Response.StatusCode >= 600
|| context.Response.ContentLength.HasValue
|| !string.IsNullOrEmpty(context.Response.ContentType))
{
return;
}
When rendering of the View starts, MVC middleware fills Response.ContentType with text/html value. That's why above check returns true and request with status code page is not re-executed.
The fix is fair simple. In your middleware that handles exceptions, call Clear() method on Response:
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception)
{
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
Sample Project on GitHub

Related

.NET Core 3.1 - redirect from middleware not working

I try to redirect to a custom error page when I get a particular status
but any request to my api is processed without problem, instead of receiving a code 301/302
even if I do the redirect on any request
public async Task InvokeAsync(HttpContext httpContext, Leads2WinDBContext context)
{
... // some sensitive identification code here
await next(httpContext);
//if (httpContext.Response.StatusCode == StatusCodes.Status404NotFound)
{
httpContext.Response.Redirect("/error");
}
}
anything I am doing wrong ?
thanks for your help
app.UseExceptionHandler() was already in my code, I just changed it to point to a custom error page

How to specify error handler for a specific API controller?

I have an ASP.NET Core application that has MVC views but also few APIs, there is already error handling defined for the application. The ErrorController is returing an HTML view, I don't want to alter this behaviour. However, I'm introducing new API controller and I would like to decide what JSON (not HTML) is being returned if any unhandled exception is being thrown.
Can I specify error handler for a given controller so it returns JSON on 500 that I want?
There are basically 2 ways to handle exceptions as a cross-cutting concern in asp.net core. One is by using IExceptionFilter (IAsyncExceptionFilter for async handling) and one is by using exception handler middleware. The exception filters are not able to handle all exceptions such as exception occurred in result execution (when serializing data, ...). So it's the most reliable to handle the exception using exception handler middleware.
You can find more about exception filter yourself, it's very easy to use for your scenario (targeting a specific controller) simply by decorating your controller class with an attribute. However here I would like to introduce how you can handle it using exception handler middleware which can be configured in the Configure method of the Startup class. Here we use IExceptionHandlerFeature to get the exception info (if any):
//exception handler for errors in other controllers & pages ...
app.UseExceptionHandler("/Error");
//exception handler for specific controllers
app.MapWhen(context => {
var controller = context.GetRouteValue("controller")?.ToString();
return controller?.ToLower() == "your_controller_name";
}, subApp => {
subApp.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
var exception = exceptionHandlerFeature?.Error;
if (exception != null)
{
//prepare the error by your own
var error = new { Messsage = exception.Message };
context.Response.ContentType = "application/json";
//set status code of your choice
//context.Response.StatusCode = ;
//write JSON response containing the error
await context.Response.WriteAsync(JsonSerializer.Serialize(error));
}
});
});
Note we use MapWhen to switch the pipeline to a terminal middleware (configured by subApp). That terminal middleware will intercept the request from being further processed by the other middlewares after it in the processing flow. You can see that the order of placing the UseExceptionHandler for other cases is important. It must be placed before the MapWhen due to how the middlewares are built along the request processing pipeline.
You can also create your own custom middelware into a separate class (following either convention-based or factory-based approach).

How to call StateHasChanged() / NotifyAuthenticationStateChanged from a class in Blazor WASM

In my project I created a class to handle http POST. The main method in the class first check if a JWT Token is available in local storage, check exp date, decide if a new toekn is needed and if so use the refresh token and finally do the POST.
I inject this class with builder.Services.AddTransient<IMyHttp, MyHttp>();
Now I would like to notify the UI (StateHasChanged() or NotifyAuthenticationStateChanged) in case the refresh token is not valid so to log out the user immediately.
The point is that I do not know how to raise the event from my http class (while from a controller is just a matter of calling this.StateHasChanged()).
As suggested here you are the (pseudo)code:
Index controller call the WebAPI to check weather:
(HttpResponseMessage, IEnumerable<WeatherForecast>) result = await MyHttp.PostPFAsync<IEnumerable<WeatherForecast>>("WeatherForecast/GetWeather", null);
This is MyHttp.PostPFAsync injected with builder.Services.AddTransient<IMyHttp, MyHttp>(); in Program.cs
public async Task<(HttpResponseMessage, T)> PostPFAsync<T>(string requestUri, object data)
{
// I get my JWT Token from localstorage, set up auth headers, create StreamContent content serializing data and then
HttpResponseMessage response = await _http.PostAsync(requestUri, content);
if (response.IsSuccessStatusCode)
{
return (response, "Happiness");
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
// Here I clear tokens from localstorage and I'd also like to logout user and update UI
}
else return (response, default(T));
}
Obviosuly I could logout and update the UI in the Index controller but this would mean to make the check everywhere I call the WebAPI via MyHttp.PostPFAsync while I would like to centralize it as soon as I get a 401 (actually I'll do this if I fail to use the refresh token but keep things simple in this example).

FOSRestBundle: Returning JSON/XML meta data for "Bad Credentials" Exception

I'm using FOSRestBundle for my REST API and so far it has been a great tool. I use HTTP Basic Auth and in most of the cases it works just fine. However, I have problems with the bundle's exception behaviour when bad credentials are submitted. When handling exceptions (via the integrated authentication handlers or the exception mapping configuration), the bundle always gives me a response with the correct HTTP status and JSON/XML content similar to this:
{
"code": 401,
"message": "You are not authenticated"
}
This is fine, it also works when no authentication information is submitted at all. However, when submitting bad credentials (e.g. unknown username or incorrect password) I get the HTTP code 401 Bad credentials (which is fine) with an empty message body. Instead, I would have expected something similar to the JSON above.
Is it a bug or a configuration issue on my side? I would also love to know how these kinds of authentication errors are exactly handled by the bundle, since overriding the BadCredentialsException's status code in the codes section of the bundle's exception configuration section seems to be ignored.
Thanks!
Alright, after digging into the bundle's code some more, I figured it out. The problem results from the way bad credentials are handled by Symfony's HTTP Basic Authentication impementation. The 401 Bad Credentials response is a custom response created by BasicAuthenticationEntryPoint, which is called by the BasicAuthenticationListener's handle function, immediately after an AuthenticationException has been thrown in the same function. So there is no way of catching this exception with a listener:
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
if (false === $username = $request->headers->get('PHP_AUTH_USER', false)) {
return;
}
if (null !== $token = $this->securityContext->getToken()) {
if ($token instanceof UsernamePasswordToken && $token->isAuthenticated() && $token->getUsername() === $username) {
return;
}
}
if (null !== $this->logger) {
$this->logger->info(sprintf('Basic Authentication Authorization header found for user "%s"', $username));
}
try {
$token = $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $request->headers->get('PHP_AUTH_PW'), $this->providerKey));
$this->securityContext->setToken($token);
} catch (AuthenticationException $failed) {
$this->securityContext->setToken(null);
if (null !== $this->logger) {
$this->logger->info(sprintf('Authentication request failed for user "%s": %s', $username, $failed->getMessage()));
}
if ($this->ignoreFailure) {
return;
}
$event->setResponse($this->authenticationEntryPoint->start($request, $failed));
}
}
The entry point's start function creates the custom response, with no exceptions involved:
public function start(Request $request, AuthenticationException $authException = null)
{
$response = new Response();
$response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', $this->realmName));
$response->setStatusCode(401, $authException ? $authException->getMessage() : null);
return $response;
}
The fist if-clause in the handle function above also explains why it works in the case of "no user credentials at all", since in that case, the listener just stops trying to authenticate the user, and therefore an exception will be thrown by Symfony's firewall listeners (not quite sure where exactly), so FOSRestBundle's AccessDeniedListener is able to catch the AuthenticationException and do its thing.
You can extend AccessDeniedListener and tell FOSRestBundle to use your own listener with the parameter %fos_rest.access_denied_listener.class%. (service definition)
parameters:
fos_rest.access_denied_listener.class: Your\Namespace\For\AccessDeniedListener
Then add an additional check for BadCredentialsException and emmit an HttpException with the desired code/message similar to the check for AuthenticationException at Line 70.

Exception thrown when WebAuthenticationBroker receives an OAuth2 callback

The WebAuthenticationBroker doesn't seem to be able to handle navigation to my ms-app://. Just throws this ugly error as you will see below.
Steps
Call AuthenticateAsync(), including callback uri obtained at runtime: WebAuthenticationBroker.GetCurrentApplicationCallbackUri()
Go through authorize process, hit Allow.
Instead of returning, the broker shows the page Can't connect to service. We can't connect to the service you need right now. Unable to do anything, so I hit the Back button visible.
Debugger breaks on catch: "The specified protocol is unknown. (Exception from HRESULT: 0x800C000D)"
The callback for WebAuthenticationBroker.AuthenticateAsync() is received (according to Fiddler4 & the Event Viewer) but it throws the aforementioned exception as if it doesn't know how to interpret the ms-app:// protocol.
All examples imply my code should work but I think there's something less obvious causing an issue.
Code
private static string authorizeString =
"https://api.imgur.com/oauth2/authorize?client_id=---------&response_type=token";
private Uri startUri = new Uri(authorizeString);
public async void RequestToken() {
try {
var war = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.UseTitle
, startUri);
// Imgur knows my redirect URI, so I am not passing it through here
if (war.ResponseStatus == WebAuthenticationStatus.Success) {
var token = war.ResponseData;
}
} catch (Exception e) { throw e; }
}
Event Viewer log excerpts (chronological order)
For information on how I obtained this, read the following MSDN: Web authentication problems (Windows). Unfortunately this is the only search result when querying authhost.exe navigation error.
Information: AuthHost redirected to URL: <ms-app://s-1-15-2-504558873-2277781482-774653033-676865894-877042302-1411577334-1137525427/#access_token=------&expires_in=3600&token_type=bearer&refresh_token=------&account_username=------> from URL: <https://api.imgur.com/oauth2/authorize?client_id=------&response_type=token> with HttpStatusCode: 302.
Error: AuthHost encountered a navigation error at URL: <https://api.imgur.com/oauth2/authorize?client_id=------&response_type=token> with StatusCode: 0x800C000D.
Information: AuthHost encountered Meta Tag: mswebdialog-title with content: <Can't connect to the service>.
Thanks for reading, Stack. Don't fail me now!
Afaik, you need to pass the end URL to AuthenticateAsync even if you assume that the remote service knows it.
The way WebAuthenticationBroker works is like the following: you specify an "endpoint" URL and when it encounters a link that starts with this URL, it will consider the authentication process complete and doesn't even try navigating to this URL anymore.
So if you specify "foo://bar" as callback URI, navigating to "foo://bar" will finish the authentication, as will "foo://barbaz", but not "foo://baz".
Resolved! #ma_il helped me understand how the broker actually evaluates the redirect callback and it led me back to square one where I realized I assumed WebAuthenticationOptions.UseTitle was the proper usage. Not so. Up against Imgur's API using a token, it requires WebAuthenticationOptions.None and it worked immediately.
As an example to future answer-seekers, here's my code.
private const string clientId = "---------";
private static Uri endUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
private static string authorizeString = "https://api.imgur.com/oauth2/authorize?"
+ "client_id="
+ clientId
+ "&response_type=token"
+ "&state=somestateyouwant"
+ "&redirect_uri="
+ endUri;
private Uri startUri = new Uri(authorizeString);
public async void RequestToken() {
try {
WebAuthenticationResult webAuthenticationResult =
await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None
, startUri
, endUri);
if (webAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success) {
string token = webAuthenticationResult.ResponseData;
// now you have the token
}
} catch { throw; }
}