Exploring Authentication in Blazor


By: Shaun Walker

Those of us who have been closely following the evolution of Blazor over the past year have been eagerly awaiting some official guidance from Microsoft in regards to application security. This was finally delivered in the .NET Core 3 Preview 6 release in June where they included some new components, examples, and documentation related to implementing authentication and authorization in Blazor applications.

The most comprehensive description of this new functionality is described by Steve Sanderson in the following Gist. Since the primary focus for the .NET Core 3 release coming this September is on the server-side hosting model, Microsoft is now allowing developers to create a server-side Blazor application in Visual Studio 2019 from a template where you can also choose to include Authentication similar to the other application .NET application templates. Chris Sainty does an excellent job of documenting the creation of such an application on his recent blog. And although the client-side Blazor hosting model is not going to be officially supported in September, Microsoft is still providing guidance to the developer community on how to use it effectively. The Blazing Pizza client-side Blazor sample application was updated to include authentication and authorization capabilities and the documentation was updated to reflect the changes.

I was excited when Preview 6 was released as I had been holding off on implementing security in Oqtane because I wanted to ensure that it was closely aligned with the official guidance from Microsoft. I closely reviewed the implementation for both the server-side and client-side hosting models. The client-side model was straightforward and I had no difficulty in integrating the code into Oqtane to create a seamless login/logout experience. The server-side model was not so straightforward. The problem is that the official guidance from Microsoft for server-side utilizes a hybrid approach, where the main application is a SPA based on Blazor components, but the login uses the default server-side Razor pages which are part of ASP.NET Core Identity. This hybrid approach might be fine for some applications but in the case of Oqtane I want the entire application to be a native SPA which offers a consistent user experience and a consistent methodology for styling and customizing the user interface. So the end result is that I needed to explore a more native approach for server-side Blazor.

One primary for goal for Oqtane has been to support both the client-side and server-side hosting models in a single codebase. So far this has been possible with only minimal conditional compilation logic in the startup class. In order to allow the user interface to interact with the back-end database in both models, the framework uses Web API services which are consumed by front-end HttpClient requests. In the server-side model this may seem like unnecessary overhead for the server to call itself using HTTP; however, I feel it is the most straightforward architectural approach for supporting both client and server models.

So the first challenge I ran into when trying to implement a native login/logout experience in server-side Blazor was directly related to the use of HttpClient. It seems that due to the order in which Blazor processes infrastructure components, it attempts to call the AuthenticationStateProvider before the UriHelper has been initialized. HttpClient relies on UriHelper to obtain its base address, and as a result the application throws a run-time error. This issue has been logged on GitHub however Steve Sanderson also suggested an effective workaround. The workaround was to create a new HttpClient rather than relying on the service registered in Startup.

The next issue I encountered was that calling the SignIn method on the Identity SignInManager did not set the auth cookie as expected. It took quite a bit of research to figure out why this was occurring. Luckily there were some helpful hints in the source code on Github in the default AuthenticationStateProvider. It appears that because of the usage of SignalR in the server-side hosting model, the HttpContext.User will stay fixed for the lifetime of the circuit. As a result, you can only implement server-side authentication with redirect-style flows since it requires a new HTTP request in order to become a different user. So basically even though there is SignalR interaction between the browser and server in the Blazor server-side model, this is specifically for dealing with DOM differences - but does not include set-cookie headers ( which are necessary for setting an auth cookie in the browser ).

So the solution to this problem is to construct a redirect-style flow for login/logout in the server-side model. The default server-side Blazor template accomplishes this by using ASP.NET Core Identity, as it essentially leverages Razor pages running on the server which then redirect back to the client browser to transit the auth cookie. But since I did not want to accept the non-native limitations of the ASP.NET Core Identity UI I needed to find an alternate option. As luck would have it, my colleague Michael Washington had previously written an insightful blog on how to implement cookie authentication in server-side Blazor. The blog was intended to be a proof of concept but it included the fundamental technique which could be used to create a native login flow in Oqtane. The concept was to include some Razor pages on the server which had no user interface functionality - they were purely for processing input sent from the client, dealing with login concerns and ultimately redirecting the flow back to the browser to set the auth cookie. In order to make Michael 's proof of concept a bit more robust it needed a few modifications.

Web security best practice recommends using POST requests for login/logout operations and Michael 's solution used a GET request for simplicity. The problem with using GET requests are numerous... it saves the login link along with the username and password in the browser 's history and it can also easily be captured by web logging components which make it more susceptible to man-in-the-middle or brute force replay attacks. So the login/logout flow needs to use a POST request; however, Blazor does not have the concept of an HTML Form. So how can we send our input to the server using a POST operation? This is precisely the type of situation where JS Interop can be used. We can include a simple JavaScript function in our application which will allow us to dynamically create an HTML Form that is submitted to the server. However this revealed a further security consideration which I had not expected. By default Razor pages expect an anti-forgery token to be passed with any Form POST. The anti-forgery token helps mitigate against cross-site request forgery attacks. And although there is a way to disable this feature on Razor pages, it obviously makes more sense to support it to ensure the application is adequately protected. The solution to this problem ended up being fairly straightforward and also took advantage of JS Interop.

First I added an antiforgery token to my _Host.cshtml using ASP.NET Core 's built-in tag helper:


Then I added a couple of new JavaScript functions to my server-side interop.js file:

window.interop = {
getElementByName: function (name) {
var elements = document.getElementsByName(name);
if (elements.length) {
return elements[0].value;
} else {
return "";
submitForm: function (path, fields) {
const form = document.createElement( 'form ');
form.method = 'post ';
form.action = path;

for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const hiddenField = document.createElement( 'input ');
hiddenField.type = 'hidden ';
hiddenField.name = key;
hiddenField.value = fields[key];


I then wrapped these functions in my Interop.cs class ( Oqtane.Client\Shared\Interop.cs ) so that I could call them from my Blazor components:

public Task<string> GetElementByName(string name)
return jsRuntime.InvokeAsync<string>(
return Task.FromResult(string.Empty);

public Task SubmitForm(string path, object fields)
path, fields);
return Task.CompletedTask;
return Task.CompletedTask;

Then in my login Blazor component ( Oqtane.Client\Modules\Admin\Login\Index.razor ) I leverage the JSInterop functions I created previously to capture the antiforgery token and construct a dynamic form which is then submitted via a POST to a Login page on the server ( you will notice that I am using an Anonymous Type to create a dynamic object rather than creating a superfluous class for this specific use case ):

var interop = new Interop(jsRuntime);
string antiforgerytoken = await interop.GetElementByName("__RequestVerificationToken");
var fields = new { __RequestVerificationToken = antiforgerytoken, username = Username, password = Password, remember = Remember, returnurl = ReturnUrl };
await interop.SubmitForm("/login/", fields);

The Login.cshtml.cs class on the server uses ASP.NET Core Identity to login the user and create the Claims Principal and redirect back to the Blazor application ( which all appears very seamless to the end user ):

public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl)
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);

bool validuser = false;
IdentityUser identityuser = await identityUserManager.FindByNameAsync(username);
if (identityuser != null)
var result = await identitySignInManager.CheckPasswordSignInAsync(identityuser, password, false);
if (result.Succeeded)
validuser = true;

if (validuser)
var claims = new List<Claim>{ new Claim(ClaimTypes.Name, username) };
var claimsIdentity = new ClaimsIdentity(claims, IdentityConstants.ApplicationScheme);
var authProperties = new AuthenticationProperties{IsPersistent = remember};
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);

return LocalRedirect(Url.Content("~/" + returnurl));

The Logout flow works much the same way ( except there is a more obvious “Attempting to reconnect to the server” message when the user is redirected in this case - which cannot be avoided due to the SignalR dependency ).

At this point my seamless login/logout experience was working. However after more thorough analysis I realized that I still had one remaining issue. As I mentioned earlier Oqtane relies on HttpClient for calling all back-end services, and I noticed that after I logged into the application the HttpClient service calls from the client were not sending the auth cookie to the Web API controller methods - HttpContext.User.IsAuthorized was always False. It was then that I realized that the HttpClient which was being created in Startup was not taking the auth cookie into consideration when it was being constructed. So I modified the HttpClient registration in Startup to include the auth cookie and it solved the final problem:

// setup HttpClient for server side in a client side compatible fashion ( with auth cookie )
if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
services.AddScoped<HttpClient>(s =>
// creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var uriHelper = s.GetRequiredService<IUriHelper>();
var httpContextAccessor = s.GetRequiredService<IHttpContextAccessor>();
var authToken = httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"];
var client = new HttpClient(new HttpClientHandler { UseCookies = false });
if (authToken != null)
client.DefaultRequestHeaders.Add("Cookie", ".AspNetCore.Identity.Application=" + authToken);
client.BaseAddress = new Uri(uriHelper.GetBaseUri());
return client;

To view a video of the user experience, please see this Twitter post by Michael Washington ( which also showcases the new Install Wizard ).

In the end it was more challenging than I expected to create a "native" login/logout experience in server-side Blazor. However I am very satisfied with the end result as it allows Oqtane to have a very consistent and seamless security implementation for both client-side and server-side hosting models. I hope other developers find this information to be useful.

An error has occurred. This application may no longer respond until reloaded. Reload 🗙