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

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
}

Related

.Net 6: Enable Windows and Anonymous authentication for one

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
}

HubConnectionContext.User property not populated for SignalR / Blazor WASM / IdentityServer hubs

I followed the tutorial Use ASP.NET Core SignalR with a hosted Blazor WebAssembly app and Authentication and authorization in ASP.NET Core SignalR to create an authenticated SignalR hub.
However I can't get the logged in user from the hub Context.User.Identity.Name object.
Upon user creation I have added a claim:
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));
In Startup.cs I have registered this IUserIdProvider and setting a breakpoint confirms it gets called during runtime but it returns null.
public class EmailBasedUserIdProvider : IUserIdProvider
{
public virtual string GetUserId(HubConnectionContext connection)
{
return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
}
}
In the hub, I have set the [Authorize] attribute but the Context.User.Identity object is null for the Name property and if I look in the Claims collection, the SQLServer user Id is present but nothing else.
I suspect the problem is in the EmailBasedUserIdProvider which is unable to find the Email claim because it is not there on the connection.User object. What needs to be done to make this available?
Thanks

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();
}

How to get the CurrentPrincipal Identity on ASP.Net Core 2 Web Application?

I created a Azure AD B2C Tenant to use with an Azure Function. I used this and it is working:
Thread.CurrentPrincipal.Identity.IsAuthenticated
Now I am trying to get the User logged in, with this same call this in ASP Net Core 2 Web Site Razor Page Index.cshtml.cs
public void OnGet()
{
var isAuth = Thread.CurrentPrincipal.Identity.IsAuthenticated;
ClaimsPrincipal cp = (ClaimsPrincipal)Thread.CurrentPrincipal;
}
But
Thread.CurrentPrincipal is returning null on ASP Net Core 2
In ConfigureServices method on Startup.cs I added
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//services.AddAuthentication();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.Audience = Configuration["Authentication:AzureAd:ClientId"];
//options.Events = new JwtBearerEvents
//{
// OnAuthenticationFailed = AuthenticationFailed
//};
var authorityBase = string.Format("https://login.microsoftonline.com/tfp/{0}/", "empresateste123.onmicrosoft.com"/*Configuration["Authentication:AzureAd:Tenant"]*/);
options.Authority = string.Format("{0}{1}/v2.0/", authorityBase, "B2C_1_policysignin " /*Configuration["Authentication:AzureAd:Policy"]*/);
});
}
In Configure method on Startup.cs I added
app.UseAuthentication();
When I publish and go to the URL, this Exception happens on that line:
An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an object.
If I use
public void OnGet()
{
//var isAuth = Thread.CurrentPrincipal.Identity.IsAuthenticated;
//ClaimsPrincipal cp = (ClaimsPrincipal)Thread.CurrentPrincipal;
var u = this.User;
var uc = u.Claims.ToList();
}
now it comes, but the Claims.Count = 0, no user information
What else do I need to add to make it work?
Use PageModel.User which returns a ClaimsPrincipal object that represents the current web-application user. Thread.CurrentPrincipal should not be used to get the current web-application user because Thread.CurrentPrincipal concerns thread security managed by .NET (and possibly the Operating System).
ASP.NET, all the way back from its early days in 2001 (ab)used this feature of .NET by overwriting Thread.CurrentPrincipal with the current ASP.NET "User" - this had practical benefits when it's used in the context of Identity Impersonation with Windows Authentication, allowing web-applications to access security-restricted files, network resources and operating-system features when it would otherwise be unable to.
This article from 2004 gives a good explanation (ignore the references to the now obsolete FormsAuthentication module): https://www.hanselman.com/blog/SystemThreadingThreadCurrentPrincipalVsSystemWebHttpContextCurrentUserOrWhyFormsAuthenticationCanBeSubtle.aspx - the article also explains why Thread.CurrentPrincipal may be null, for example when application code is called before HttpApplication.OnThreadEnter() (which sets the CurrentPrincipal property) is called.
I'm not too familiar with the lifecycle of Razor-Pages (a new feature in ASP.NET Core since late 2017) or how the security system changes in an Azure Functions and Azure AppService (f.k.a. Azure Websites) context.
In any event, the fix is to always use the User property in ASP.NET instead of Thread.CurrentPrincipal.
In ASP.NET Web Forms, use System.Web.HttpContext::User (or System.Web.UI.Page::User)
In ASP.NET MVC, use System.Web.Mvc.Controller::User
ASP.NET MVC (when using .aspx Views) can use System.Web.Mvc.ViewPage::User
ASP.NET MVC using Razor Views can access #this.User (or just #User) directly in Razor code (inherited from System.Web.WebPages.WebPageRenderingBase::User)
In ASP.NET Core MVC, use Microsoft.AspNetCore.Mvc.ControllerBase::User inside an Action.
In ASP.NET Razor-Pages, use Microsoft.AspNetCore.Mvc.RazorPages.PageModel::User.

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.