Blazor Server and SignalR and Azure AD - asp.net-core

I am working on a web application using Blazor Server .Net 5. On my index page, I need to show the number of online users that logged into the website through Azure AD.
First, the user hits the web, and it gets redirected to Azure AD. Once the user is Authenticated in AD he/she will land on the index page. I want to show number of online users inside the app. I started using SignalR, but I am getting a very weird Error.
I am using SingalR client lib
First I created the
PeoplHub : Hub{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Then in my Index.razor I have created
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
I have also Implemented the IAsyncDisposal
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
in my startup I implemented
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
app.UseResponseCompression();
endpoints.MapHub<PeopleHub>("/peoplehub");
When I run the app, I get this error message
An unhandled exception occurred while processing the request.
JsonReaderException: '<' is an invalid start of a value. LineNumber: 2 | BytePositionInLine: 0.
System.Text.Json.ThrowHelper.ThrowJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte, ReadOnlySpan<byte> bytes)
InvalidDataException: Invalid negotiation response received.
Microsoft.AspNetCore.Http.Connections.NegotiateProtocol.ParseResponse(ReadOnlySpan<byte> content)

After researching on this issue. I found some useful information. We don't know the known issue, you can create a support ticket and ask for help.
It turns out that there is a known issue breaking SignalR Hubs with Blazor Server and Microsoft Identity.
And I also find official engineer said they don't plan to make improvements in this area given that we haven't seen many customers hitting it.
Related Issue:
blazor server signalr JsonReaderException
Workaround
ASP.NET Core Blazor Server additional security scenarios

Adding on to the answer by Jason Pan.
A quick way to validate the authorization is the problem.
Since I knew my code worked without Authorization in a dotnet 7 app
and this error was seen when I moved the code into my production code (dotnet 6)
where we use authorization with Azure AD
I ran a test with "AllowAnymous" on the hub.
[AllowAnonymous()] //TODO: authorize...
public class SignalrHub : Hub
{
and everything works as expected.
Next : follow the workaround as posted by Jason

Related

How to configure Azure AD authentication in Hybrid ASP.NET Core MVC (backend) and Vuejs SPA (frontend)?

My application is a hybrid approach where use ASP.NET Core MVC as my backend. I have various controllers which my front end uses to pull data from our database and also to do API calls on MS Graph. I am using the following program.cs file to get the authentication initiated when a user first logs on to the site:
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
}).EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
}).AddMicrosoftIdentityUI();
// Add the UI support to handle claims challenges
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddRequiredScopeAuthorization();
In the Azure AD portal my application is registered as a web app. So when a user initially goes to the site they are redirected to https://login.microsoftonline.com/blahblah to get the login process started. This is automated by the Azure AD identity platform. Then once the login occurs they are redirected to localhost where the VueJS spa is loaded (localhost:43862). My spa uses various axios requests to the controllers and they pull data and vue router loads components. However, my issue is say the user needs to relog in because the cookie is expired or they logged out in another tab. The next axios request made by the expired session does not redirect the user to Azure login screen but instead results in an CORS error. So I need to get my axios requests to force the page redirect to Azure AD login screen (which probably is the worst idea since CORS policy is resulting in error) or have it return a redirect to localhost/login which is my own custom login screen with a button to Azure AD login and shouldnt impact CORS. So how do I intercept this Azure AD redirect to Azure AD login and replace with my own?
I have also tried to return a 401 error code so I could check for that in my axios request but to no avail it does nothing. If I put a breakpoint there it does hit this code but it does not change the status code of the response and I still get 302. My code for that was to try and add to the event :
OnRedirectToIdentityProvider = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
My other ideas was maybe I should set my CORS policy to allow redirects from login.microsoft.com? Or would this be bad practice?
I can answer part of your question... First, for our API application which is protected by Azure AD, what the API should do is validating the request whether it contained a correct access token in the request header, if yes, give the response, if no, then give error like 401 or 403. A normal API application shouldn't have a UI to let users sign in. Anyway, if you want to expose an API in an MVC project, it's OK, but for API itself, it shouldn't have a UI.
Let's see sample below, I had a .net 6 web api project, and here's my program.cs:
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
And it requires configurations in appsetting.json.
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "azure_ad_client_id",
"ClientSecret": "client_secret",
"Domain": "tenant_id",
"TenantId": "tenant_id",
//"Audience": "api://azure_ad_client_id_which exposed_api" // here I used the same azure ad app to expose API, so I can comment this property
},
And this is the Controller:
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
[RequiredScope("Tiny.Read")]
[HttpGet]
public string Get()
{
return "world";
}
}
I had an Azure AD app, and I exposed an API like this:
I also add this API for the same Azure AD app.
Then let's do a test. When I call this API directly, I will get 401 error:
If I used an expired token within the request, I will also get 401 error:
But if I used a correct token(go to https://jwt.io to decode the token, we should see it containing correct scope, for me its "scp": "Tiny.Read",), I will get response:
And till now, the API part had finished. Let's see the client SPA. For SPA, you should integrate MSAL so that you can make your users to sign in via Azure AD, and generate the access token for calling MS graph API or your own API. The code for generating access token should be the same but you should set different scope for different API. In my scenario, my API required a scope Tiny.Read, then I should set in my client App.
Here's an screenshot for generating access token in react. You need to set the scope in your code.
Now you have the method to generate access token, you already know the API url. Then you can send request to call api, using AJAX, using fetch, or something else, sending an http request is ok. And in the calling api part, you also need to handle the response. If the response code is 401, then you need to do some logic, maybe redirect to the sign in page. And you said you had trouble here, you met CORS issue. I can't answer this part. I think it depends on how you redirect to Azure AD sign in page. I'm afraid you can take a look at this sample to learn how to sign in users and call graph api.

.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
}

Blazor WASM standalone project reauthentication for multiple browser tabs after logout

I have implemented authorization with JWT auth on our blazor WASM project. All was well - logging in and logging out...until
Scenario:
I noticed that if I duplicated my 'logged in (authenticated identity user) tab', now having two valid authorized user tabs open. In the first tab I log out: I am re-directed to the login page, the stored token is gone, everything seems ok - but when I go to the second tab and click to go to a new page(within the tab) I am able to do so. When I debug and check the auth state.. it still has the validated identity user!
Expected Results:
When I logout in the first autenticated tab, I expect the auth state of the second tab to also be deauthenticated.
Error Messages:
Currently none
What I have tried:
I searched around for someone solving the same thing for Blazor WASM standalone apps. I did come across this guy's video where he describes most people are using the AuthenticationStateProvider wrong by injecting it directly instead of using a cascading parameter. He actually then demonstates the exact issue I am having - but then proceeds to solve it with a Balzor Server class library(using RevalidateServerAuthenticationStateProvider with some custom code on the ValidateAuthenticationStateAsync function)! Not a WASM solution! Link to his video: https://www.youtube.com/watch?v=42O7rECc87o&list=WL&index=37&t=952s - 13:23 he demonstrates the issue I have attempted to depict.
Code of my logout sequence:
To start - when you click the logout button - I redirect to a logout page. We will start here:
Logout.Razor.cs
[CascadingParameter]
public Task<AuthenticationState> AuthenticationStateTask { get; set; }
protected override async Task OnInitializedAsync()
{
await AuthService.Logout();
var authState = await AuthenticationStateTask;
var user = authState.User;
if (!user.Identity.IsAuthenticated)
{
NavManager.NavigateTo("/");
}
}
You can see you then are waiting for the AuthService to log you out.
AuthService.cs
private readonly AuthenticationStateProvider _authStateProvider;
public async Task Logout()
{
await _localStorage.RemoveItemAsync("authToken");
await ((CustomAuthStateProvider)_authStateProvider).NotifyUserLogout();
_client.DefaultRequestHeaders.Authorization = null;
}
Then you are waiting on the CustomAuthStateProvider:AuthenticationStateProdiver to notify that the state has changed.
CustomAuthStateProvider.cs
_anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
public async Task NotifyUserLogout()
{
var authState = Task.FromResult(_anonymous);
NotifyAuthenticationStateChanged(authState);
}
At this point in the active page we are logged out and redirected to the login page.
I was hoping there is some browser session option for the authstate I am missing or there is some microsoft documentation of handling this situation on WASM projects - but my peanut brain can't seem to find it.
Additional Information: I am purely only using the AuthenticationStateProdiver for a custom login.
I am not using OidcAuthentication or MsalAuthentication services.
This is a standlone WASM app with a completely decoupled API. All .net6 living on azure.
Looking forward to see if anyone else has this issue!
BR,
MP

Windows authentication fail with "401 Unauthorized"

I have a MVC client accessing a Web API protected by IDS4. They all run on my local machine and hosted by IIS. The app works fine when using local identity for authentication. But when I try to use Windows authentication, I keep getting "401 Unauthorized" error from the dev tool and the login box keeps coming back to the browser.
Here is the Windows Authentication IIS setting
and enabled providers
It's almost like that the user ID or password was wrong, but that's nearly impossible because that's the domain user ID and password I use for logging into the system all the time. Besides, according to my reading, Windows Authentication is supposed to be "automatic", which means I will be authenticated silently without a login box in the first place.
Update
I enabled the IIS request tracing and here is the result from the log:
As you can see from the trace log item #29, the authentication (with the user ID I typed in, "DOM\Jack.Backer") was successful. However, some authorization item (#48) failed after that. And here is the detail of the failed item:
What's interesting is that the ErrorCode says that the operation (whatever it is) completed successfully, but still I received a warning with a HttpStatus=401 and a HttpReason=Unauthorized. Apparently, this is what failed my Windows Authentication. But what is this authorization about and how do I fix it?
In case anyone interested - I finally figured this one out. It is because the code that I downloaded from IndentityServer4's quickstart site in late 2020 doesn't have some of the important pieces needed for Windows authentication. Here is what I had to add to the Challenge function of the ExternalController class
and here is the ProcessWindowsLoginAsync function
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
Now my windows authentication works with no issues.

ASP.NET Core Entity Framework User Impersonation Failing

I'm developing an ASP.NET Core webapp that requires forwarding the user's window's credentials to use for database access through Entity Framework. But an exception is thrown on database access. System.Data.SqlClient.SqlException: 'login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'.'.
I have tried using impersonation in middleware, as well as around the query itself. The way I invoke it is:
using (WindowsImpersonationContext impersonationContext = ((WindowsIdentity)User.Identity).Impersonate())
{
var debug2 = WindowsIdentity.GetCurrent(); // shows the desired user
entiryFrameworkContext.BlahBlahBlah.ToList();
}
Or if I'm taking the middleware approach:
app.Use(async (context, next) =>
{
using (WindowsImpersonationContext impersonationContext = ((WindowsIdentity)context.User.Identity).Impersonate())
{
var debug = WindowsIdentity.GetCurrent(); // shows the desired user
await next.Invoke();
}
});
The results are the same whether in release mode pushed to IIS or running in VS2017 IIS Express.
Does anyone have an idea why the anonymous user is still used for db calls? And if so, how do I fix it?