.NET Core 3 (preview) Web API return UnAuthorized (401) instead of NotFound (404) - authentication

Having read through many posts, blogs and this SO thread, this code doesn't do what I expect it to do:
services.AddAuthentication().AddCookie(options =>
{
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
context.Response.Clear();
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Clear();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
An excerpt from an API controller (using the authorize attribute):
[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "User")]
public class TravelPlanController : BaseController{
...
}
This is part of the startup configuration of a Web API in .NET Core 3.x (preview) and should return a 401 UnAuthorized (which essentially should be UnAuthenticated) but instead returns a 404 NotFound.
The 404 results from the fact that the default .NET Core Authentication Middleware redirects to something like /auth/login and that route is not available (by design; it is an API not a MVC website). So the request is unauthorized, gets redirected by default, and results in a 404 :s
Both the apporaches of OnRedirectToLogin handlers should intercept this default behaviour, which is odd for a RESTfull API, and return a simple 401 UnAuthorized. But they don't, breakpoint isn't hit in debug mode, Postman and an Angular app in Chrome both report a 404.
Did anything change since .NET Core 3.x? Or did the solutions from others never really work.

This did the trick:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityContext>()
.AddDefaultTokenProviders()
.AddRoles<IdentityRole>();
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
Working with ASP.NET Core 3.x (preview) and the default Identity provider the order in which the above declarations are specified makes the difference for the OnRedirectToLogin event to be fired.
I eloborated on this in this SO thread as well.

Try to define the OnRedirectToAccessDenied on the Events as you did for OnRedirectToLogin.

Related

Keycloak Log out from ASP.NET and ASP.NET Core

Currently I am able to login from ASP.NET and ASP.NET Core. However when logout from ASP.NET, my ASP.NET Core app doesn't logout as well.
Here is my ASP.NET logout code:
public ActionResult logout()
{
Request.GetOwinContext().Authentication.SignOut(HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes().Select(o => o.AuthenticationType).ToArray());
return RedirectToAction("About", "Home");
}
And my ASP.NET Core logout:
public IActionResult Logout()
{
return new SignOutResult(new[] { "OpenIdConnect", "Cookies" });
}
Unfortunately, if I logout from the ASP.NET app, my ASP.NET Core app doesn't logout automatically. Is it something wrong with my keycloak setting, or did I miss something in my code?
Go through the https://github.com/dotnet/aspnetcore/blob/4fa5a228cfeb52926b30a2741b99112a64454b36/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L312-L315
services.AddAuthentication(...)
.AddCookie("Cookies")
.AddOpenIdConnect("OpenIdConnect", options =>
{
...
options.Events.OnSignedOutCallbackRedirect += context =>
{
context.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
...
});
It is working for me. I used code similar to yours:
public IActionResult Logout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme
});
}
If you get invalid redirect error from keycloak, then you must also add Valid post logout redirect URIs to your Keycloak client settings. In your case, you have to add your_host/signout-callback-oidc
If you get an invalid or missing id_token_hint parameter, then make sure that tokens are being saved:
.AddOpenIdConnect(options =>
{
options.SaveTokens = true;
});

Asp.net core exception handling in applications that combines Razor Pages and ApiControllers

when using asp.net applications that combines Razor Pages and Api Controllers.
how to globally check if the exception is thrown from an Api Controller ?
the idea is to use UseExceptionHandler midlleware but conditionally return an html response if the unhanded exception is thrown from a Razor Page and a json ProblemDetails response if the excpetion is thrown from an ApiController
For web Api, add attribute route with Api, and then check the request path in the middleware or exception handler like this:
app.UseExceptionHandler("/Error"); //handle the exception from the razor page
//handle the exception from the API.
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), subApp =>
{
subApp.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"error\":\"Exception from API!\"}");
//await context.Response.WriteAsync("ERROR From API!<br><br>\r\n");
//await context.Response.WriteAsync("Home<br>\r\n");
//await context.Response.WriteAsync("</body></html>\r\n");
});
});
});
The result as below:
Besides, you can also use a custom exception handler page is to provide a lambda to UseExceptionHandler. Using a lambda allows access to the path of the request that made the error before returning the response.
For example:
//app.UseExceptionHandler("/Home/Error");
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
//check if the handler path contains api or not.
if (exceptionHandlerPathFeature.Path.Contains("api"))
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; ;
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
await context.Response.WriteAsync("ERROR From API!<br><br>\r\n");
await context.Response.WriteAsync(
"Home<br>\r\n");
await context.Response.WriteAsync("</body></html>\r\n");
}
else
{
context.Response.Redirect("/Home/Error");
}
});
});
More detail information, see asp.net core app.UseExceptionHandler() to handle exceptions for certain endpoints?

In ASP Net Core 3.1 Expiration cookie is not redirecting to login page when using ajax

In my app, when my cookie expire, I'm redirect to my Account/Login page. But When I call ajax method and cookie is expired , the action return 401 and I'm not redirecting to my Account/login page...
I add [Authorize] attribute on my controller.
The xhr.status parameter return 401.
Example ajax method :
$(document).on('click', '.ajax-modal', function (event) {
var url = $(this).data('url');
var id = $(this).attr('data-content');
if (id != null)
url = url + '/' + id;
$.get(url)
.done(
function (data) {
placeholderElement.html(data);
placeholderElement.find('.modal').modal('show');
}
)
.fail(
function (xhr, httpStatusMessage, customErrorMessage) {
selectErrorPage(xhr.status);
}
);
});
My ConfigureServices method :
public void ConfigureServices(IServiceCollection services)
{
#region Session
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(1000);
options.Cookie.HttpOnly = true; // permet d'empecher à du code JS d'accèder aux cookies
// Make the session cookie essential
options.Cookie.IsEssential = true;
});
#endregion
#region Cookie
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "TestCookie";
options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
options.LoginPath = "/Account/login";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.SameSite = SameSiteMode.Strict;
});
#endregion
Thanks for your help
I came across the issue where I am using cookie authentication in .NET Core 5, yet once the user is authenticated, everything BUT any initial AJAX request in the application works.
Every AJAX request would result in a 401. Even using the jQuery load feature would result in a 401, which was just a GET request to a controller with the [Authorize(Role = "My Role")]
However, I found that I could retrieve the data if I grabbed the URL directly and pasted it in the browser. Then suddenly, all my AJAX worked for the life of the cookie. I noticed the difference in some of the AJAX posts. The ones that didn't work used AspNetCore.AntiForgery in the headers, whereas the ones that did use AspNetCore.Cookies that authenticated.
My fix was to add a redirect in the OnRedirectToLogin event under cookie authentication. It works for all synchronous and asynchronous calls ensuring that AJAX redirects to the login page and authenticates as the current user. I don't know if this is the proper way to handle my issue, but here is the code.
EDIT: I should mention that all of the AJAX code worked perfectly in my .NET 4 web application. When I changed to 5, I experienced new issues.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => {
o.LoginPath = "/Account/Login";
o.LogoutPath = "/Account/Logout";
o.AccessDeniedPath = "/Error/AccessDenied";
o.SlidingExpiration = true;
//add this to force and request to redirect (my purpose AJAX not going to login page on request and authenticating)
o.Events.OnRedirectToLogin = (context) => {
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

Disable HTTP Options method in ASP.Net Core 3.1

We have a requirement of disabling the HTTP OPTIONS method in an ASPNET Core Web application due as a part of security fixes. How can we disable the HTTP OPTIONS method in ASP.Net core 3.1 API?
Here is a demo with middleware:
Add this to your startup Configure:
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
if (context.Request.Method=="OPTIONS")
{
context.Response.StatusCode = 405;
return;
}
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
result:
Or you can apply
[HttpGet]
[HttpPost]
[HttpPut]
...
on your action method in controller.Here is an official document about the Http Verbs.

ASP.Net Core Identity with JwtBearer AuthenticationScheme map claims to context User object

I have a React Front end using the msal lib to authenticate the user client side with our Azure AD. This works great and authentication has no issues. I also have an ASP.Net Core WebApi to provide data to the client. I am using the JwtTokens to pass the Bearer token in the request. The WebApi is able to validate the token and all is well... I thought, however, when the WebApi method is invoked the only way I can get the User's email or name is to query the User.Claims with Linq.
this.User.Claims.Where(c=> c.Type == "preferred_username").FirstOrDefault().Value
I was about to go down the road of mapping these linq queries to an object which could be injected into the WebApi's controller, but that seems wrong.
I am obviously missing something in my Startup.cs for the WebApi, Any help or suggestions would be great!:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//add authentication JwtBearer Scheme
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = Configuration["JwtSettings:Audience"];
options.Authority = Configuration["JwtSettings:Authority"];
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
//log
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
//log
return Task.CompletedTask;
}
};
options.SaveToken = true;
});
services.AddAuthorization();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}