.Net 6: Enable Windows and Anonymous authentication for one - authentication

I work on a .Net core application and I need to mix windows and anonymous authentication within the same endpoint(s). So the goal is to be able to determine the windows user but the endpoint should also work when no windows user is present (aka windows authentication fails).
My problem is that when I use the Authorize attribe (as shown in the example below), the endpoint will only be called when windows authentication succeded. If I additionaly add the [AllowAnonymous] attribute, the User is never authenticated.
Example: (
[Authorize(AuthenticationSchemes = IISDefaults.AuthenticationScheme)]
public IActionResult Index()
{
_log.LogDebug("IsAuthenticated = " + this.User.Identity.IsAuthenticated.ToString());
_log.LogDebug("Authenticated Name: " + this.User.Identity.IsAuthenticated.Name);
return View();
}
How can this be done in .Net 6.0? It should be really simple as authentication and authorization should be separated but it seems they are quite intertwined. I haven't found a solution after extensive googling, checking the .net core source code and trying out myself.
Is there a good way to solve this?
Remark 1: there are solutions for .Net core 3.1 but then don't work in .Net 6 Enable both Windows authentication and Anonymous authentication in an ASP.NET Core app
Remark 2: we have endpoint that have to work with Windows Authentication only and other with anonyomous authentication. These both work fine within the same application. It is really about being able to detect the windows user in an endpoint that otherwise supports anymous authentication.

I (or better we) have found a solution that works even when Windows authentication is disabled on IIS. It is not very elegant but this is what we came up with. The idea is basically to trigger another call to an endpoint to determine if the user is actually a windows loging or not. If this call is successful, then we know we have a windows user and can act accordingly, for example do a redirect to an endpoint that requires windows authentication.
Remark: If you can control the IIS settings - which probably is often the case - , then I suggest you go with the solution proposed here:
enable-both-windows-authentication-and-anonymous-authentication-in-an-asp-net-co )
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TestWindowsAuthAsync(CancellationToken cancellationToken)
{
using var client = new HttpClient(new HttpClientHandler()
{
UseDefaultCredentials = true
});
var response = await client.GetAsync($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.PathBase}{HttpContext.Request.Path}/HasUserWindowsAuth");
if (response.IsSuccessStatusCode)
{
// Yes, now we know that user indeed has windows authentication and we can act upon it
return RedirectToAction("QuickLogin", input);
}
// No windows credentials have been passed at this point
return View();
}
[HttpGet("HasUserWindowsAuth")]
[Authorize(AuthenticationSchemes = IISDefaults.AuthenticationScheme)]
public IActionResult HasUserWindowsAuth() => Ok();
[HttpGet("QuickLogin")]
[Authorize(AuthenticationSchemes = IISDefaults.AuthenticationScheme)]
public async Task<IActionResult> QuickLoginAsync(LoginModel input, CancellationToken cancellationToken)
{
var user = this.User.Identities.FirstOrDefault(i => i System.Security.Principal.WindowsIdentity && i.IsAuthenticatd);
// do something with that user
}

Related

Blazor Server / Asp.Net Core: Http Request doesn't pass user identity to MVC Controller when published on IIS

currently I'm having trouble getting identity information in my MVC controllers. It's no problem when debugging the blazor application locally but when I publish the application on IIS, I only get the identity of the executing service account, for instance "MachineName\ApplicationPoolIdentity" or "MachineName\LocalService" (depending on what was selected as identity in the application pool settings) in my MVC controllers. On the Blazor pages, however, authentication and authorization seems to work fine.
I got "Windows Authentication" enabled and "Anonymous Authentication" disabled in IIS site authentication settings.
I need the users identity for our audit trail implementation, which creates an entry for each crud operation.
In the MVC Controller I tried using "this.User.Identity" or "HttpContext.User.Identity", which is the same object. When debugging locally it shows the corect identity (of myself as caller). When deployed on IIS I get "MachineName\ApplicationPoolIdentity"
the MVC controller looks as such:
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class DatabaseSystemsController : CustomControllerBase
{
// GET: api/DatabaseSystems
[HttpGet("/api/AllDatabaseSystems")]
public async Task<ActionResult<IEnumerable<DatabaseSystem>>> GetAllDatabaseSystems()
{
try
{
var identity = HttpContext.User.Identity
...
return await _context.DatabaseSystems.ToListAsync();
}
catch (Exception)
{
...
}
}
}
I hope someone can help.
Thanks in advance
L.
To get the user to impersonate use the AuthenticationStateProvider and get the user from this and cast to a WindowsIdentity to retrieve the AccessToken.
This works in both a controller and a razor component.
Inject the AuthenticationStateProvider and then in your method use the following code:
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var userToImpersonate = (WindowsIdentity)user.Identity;
await WindowsIdentity.RunImpersonatedAsync(userToImpersonate.AccessToken, async () =>
{
// Your Code in here to call your api
}

How to handle cancel button of windows authentication pop-up in asp.net core mvc deployed on Kestrel?

I'm trying to implement mixed authentication windows + cookie based in asp.net core mvc application. When windows authentication is canceled I want to redirect user to fallback page where can choose windows or cookie based authentication. The app will be deployed on Kestrel not IIS. I'm using .net core 3.1.
Basically I need to redirect user on fallback page when http status code is 401 and substatus code is 1 or 2.
So far I tried to use status code pages as fallows:
In Startup.cs I added
app.UseStatusCodePagesWithReExecute("/Error/Status", "?statusCode={0}");
In ErrorController.cs
public IActionResult Status(int statusCode)
{
var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCode == 401)
{
return RedirectToAction("Index", "Fallback");
}
var originalURL =
statusCodeReExecuteFeature.OriginalPathBase
+ statusCodeReExecuteFeature.OriginalPath
+ statusCodeReExecuteFeature.OriginalQueryString;
return Redirect(originalURL);
}
This not works because it breaks windows authentication handshake.
I also tried to implement custom error handler and check HttpResponse.StatusCode == 401, but the error controller method is not hit probably because windows authentication is handled by another layer ..
It took me a few days before I almost thought it can't be done. But finally I figured it out.
My problem was when executing External Authentication on Identity Server 4. I couldn't catch the event when the user clicks "Cancel". I got 401 status code for entering Windows Authentication, for clicking "Cancel" and also when clicking "Signin". The solution is the following:
Place this line in Startup.cs in public void Configure(IApplicationBuilder app):
app.UseStatusCodePagesWithReExecute("/external/ErrorPage", "?statusCode={0}");
After that just create a IActionResult function in controller like this:
[HttpGet]
public IActionResult ErrorPage(int? statusCode = null)
{
if (statusCode.HasValue)
{
return View(new SomeViewModel() { ErrorMessage = statusCode.Value.ToString()});
}
return View();
}

OpenIDDict Registered User Email Confirmation

I'm using ASP.NET MVC Core with OpenIDDict. I'm employing views with cookie authentication and JWT for the OpenIDDict. Everything works perfectly save for the following scenerio:
Identity is changed to send a confirmation email to the user first to confirm their email address. The user gets the link by email and clicks it. They then login. Simple. In the meantime they can't access any pages.
But I noticed, using postman, they can access the solution via APi'S through OpenIDDict. They still get authorised. And as they get authorised they can access APIs.
Having just written this I've had a brain waive and I could simply introduce the code into the AuthenticationController during the exchange action:
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
//if (user == null)
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
Works ! There u go...answered my own question. Mya help others in future
While your solution works, you might prefer configuring ASP.NET Core Identity to make the "email confirmed" check for you when calling SignInManager.CanSignInAsync(user). You can configure that in the Identity options:
services.AddIdentity<ApplicationUser, IdentityRole>(options => {
options.SignIn.RequireConfirmedEmail = true;
});

Claims based authentication, with active directory, without ADFS

I have a client asking for an integrated authentication based solution utilizing a custom role/membership schema. My original plan was to use claims based authentication mechanism with integrated authentication. However, my initial research is not turning up a whole lot of useful information.
To the point, I have an ASP.NET (not core nor owin) WebAPI application, which has api actions used by angular SPA based (asp.net) web application. I am attempting to authorize the api calls using integrated authentication. My initial effort was focused around a custom AuthorizationAttribute and ClaimsAuthenticationManager implementation. However as I got deeper into that I started running into issues with the custom ClaimsAuthenticationManager, at this point I'm not sure that is the proper route to take.
So my question for you all is, can you at least give me some ideas of what it would take to make this happen? I don't need help with secific bits the code, just need to figure out the appropriate "stack" so to speak.
The only real requirement is WebAPI calls can be authorized, with a custom attribute passing a name of a claim to authorize on, but the claim is not in AD even though it is using windows authentication, the claims themselves would come from a database.
Thank you all in advance!
Look at https://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api.
Your scenario isn't much different:
you're using AD for authentication
you're using your db for authorization
Simply put this can be addressed by configuring web-api to use windows authentication.
<system.web>
<authentication mode="Windows" />
</system.web>
And add your own IAuthorizationFilter to Web API pipeline, that will check current principal (should be set), and then override this principal with your own (i.e. query db - get claims, and override it with your custom claims principal by setting HttpContext.Current.User and Thread.CurrentPrincipal).
For how to add filter to WebAPI pipe line check out How to add global ASP.Net Web Api Filters?
public class CustomAuthenticationFilter : IAuthenticationFilter {
public bool AllowMultiple { get { return true; } }
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) {
var windowsPrincipal = context.Principal as WindowsPrincipal;
if (windowsPrincipal != null) {
var name = windowsPrincipal.Identity.Name;
// TODO: fetch claims from db (i guess based on name)
var identity = new ClaimsIdentity(windowsPrincipal.Identity);
identity.AddClaim(new Claim("db-crazy-claim", "db-value"));
var claimsPrincipal = new ClaimsPrincipal(identity);
// here is the punchline - we're replacing original windows principal
// with our own claims principal
context.Principal = claimsPrincipal;
}
return Task.FromResult(0);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) {
return Task.FromResult(0);
}
}
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.Filters.Add(new CustomAuthenticationFilter());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute( ... );
}
}
Also there is no need for custom authorization attribute - use default one - its understood by everyone, and makes your code more readable.

Aspnet Core with Adfs 2016 OpenId can't sign out

I setup an MVC project with Aspnet Core targeting Net461. Authentication is configured to use Adfs from a Windows Server 2016 system. I managed to get sign in working, however, when I click sign out I am given a page cannot be displayed error. Browsing back to the home url shows that the user is still logged in also. Any suggestions?
You might find this sample useful (even though it is for Azure ADFS, it works for local installs as well): https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore
The logout action method like the following work well in my case:
[HttpGet]
public IActionResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
This will redirect you to the /Account/SignedOut after it completes and you need to register your /signout-callback-oidc endpoint for your client as well. This endpoint is used (by default) by the OIDC ASP.NET Core middleware.