12/18/2019 Admin

A Demonstration of Simple Server-side Blazor Cookie Authentication


To demonstrate how authentication works in a server-side Blazor application, we will strip authentication down to its most basic elements. We will simply set a cookie then read that cookie in the application.

Application Authentication

image

Most business web applications require their users to log into the application.

image

The user enters their username and password, that are checked against a membership database.

image

Once authenticated, the application recognizes the user and now has the ability to deliver content securely.

Once the authentication process of a server-side Blazor application is understood, we can then implement an authentication and membership management system that meets our needs (for example, one that allows users to create and manage their user accounts).

NOTE: This sample code does not check to see if a person is using a legitimate username and password! You would need to add the proper code to check. This code is just a demonstration of how the process of authorizing a user works.

Create The Application

image

Open Visual Studio 2019.

image

Create a Blazor Server App without authentication.

Add Nuget Packages

image

In the Solution Explorer, right-click on the client project and select Manage NuGet Packages.

image

Add references to the following libraries:

  • Microsoft.AspNetCore.Authorization
  • Microsoft.AspNetCore.Http
  • Microsoft.AspNetCore.Identity

image

And:

  • Microsoft.AspNetCore.Blazor.HttpClient

Add Cookie Authentication

image

Open the Startup.cs file.

Add the following using statements to the top of the file:

								
									// ******
								
									// BLAZOR COOKIE Auth Code (begin)
								
									using
									Microsoft.AspNetCore.Authentication.Cookies;
								
									using
									Microsoft.AspNetCore.Http;
								
									using
									System.Net.Http;
								
									// BLAZOR COOKIE Auth Code (end)
								
									// ******
								

							

Alter the Startup class to the following, adding the sections marked BLAZOR COOKIE Auth Code:

								
									public
									class
									Startup
								
									{
								
									public
									Startup(IConfiguration configuration)
								
									{
								
									Configuration = configuration;
								
									}
								
									public
									IConfiguration Configuration {
									get; }
								
									// This method gets called by the runtime. Use this method to
									
								
									// add services to the container.
								
									// For more information on how to configure your application,
									
								
									// visit https://go.microsoft.com/fwlink/?LinkID=398940
								
									public
									void
									ConfigureServices(IServiceCollection services)
								
									{
								
									// ******
								
									// BLAZOR COOKIE Auth Code (begin)
								
									services.Configure<CookiePolicyOptions>(options =>
								
									{
								
									options.CheckConsentNeeded = context =>
									true;
								
									options.MinimumSameSitePolicy = SameSiteMode.None;
								
									});
								
									services.AddAuthentication(
								
									CookieAuthenticationDefaults.AuthenticationScheme)
								
									.AddCookie();
								
									// BLAZOR COOKIE Auth Code (end)
								
									// ******
								
									services.AddRazorPages();
								
									services.AddServerSideBlazor();
								
									services.AddSingleton<WeatherForecastService>();
								
									// ******
								
									// BLAZOR COOKIE Auth Code (begin)
								
									// From: https://github.com/aspnet/Blazor/issues/1554
								
									// HttpContextAccessor
								
									services.AddHttpContextAccessor();
								
									services.AddScoped<HttpContextAccessor>();
								
									services.AddHttpClient();
								
									services.AddScoped<HttpClient>();
								
									// BLAZOR COOKIE Auth Code (end)
								
									// ******
								
									}
								
									// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
								
									public
									void
									Configure(IApplicationBuilder app, IWebHostEnvironment env)
								
									{
								
									if
									(env.IsDevelopment())
								
									{
								
									app.UseDeveloperExceptionPage();
								
									}
								
									else
								
									{
								
									app.UseExceptionHandler("/Error");
								
									// The default HSTS value is 30 days.
									
								
									// You may want to change this for production scenarios,
									
								
									// see https://aka.ms/aspnetcore-hsts.
								
									app.UseHsts();
								
									}
								
									app.UseHttpsRedirection();
								
									app.UseStaticFiles();
								
									app.UseRouting();
								
									// ******
								
									// BLAZOR COOKIE Auth Code (begin)
								
									app.UseHttpsRedirection();
								
									app.UseStaticFiles();
								
									app.UseCookiePolicy();
								
									app.UseAuthentication();
								
									// BLAZOR COOKIE Auth Code (end)
								
									// ******
								
									app.UseEndpoints(endpoints =>
								
									{
								
									endpoints.MapBlazorHub();
								
									endpoints.MapFallbackToPage("/_Host");
								
									});
								
									}
								
									}
								

								

								

							
							

First the code adds support for cookies. Cookies are created by the application, and passed to the user’s web browser when the user logs in. The web browser passes the cookie back to the application to indicate that the user is authenticated. When the user ‘logs out’, the cookie is removed.

This code also adds:

  • HttpContextAccessor
  • HttpClient

as services that will be accessed in the code using dependency Injection .

See this link for a full explanation of how HttpContextAccessor allows us to determine who the logged in user is.

Add Login/Logout Pages

image

Logging in (and out) is performed by .cshtml pages.

Add the following Razor pages and code:

Login.cshtml

								
@page
								
@model BlazorCookieAuth.Server.Pages.LoginModel
								
@{
								
									ViewData["Title"] = "Log in";
								
}
								
									<
									h2
									>Login</
									h2
									>
								
								

							

Login.cshtml.cs

								
									using
									System;
								
									using
									System.Collections.Generic;
								
									using
									System.Security.Claims;
								
									using
									System.Threading.Tasks;
								
									using
									Microsoft.AspNetCore.Authentication;
								
									using
									Microsoft.AspNetCore.Authentication.Cookies;
								
									using
									Microsoft.AspNetCore.Authorization;
								
									using
									Microsoft.AspNetCore.Mvc;
								
									using
									Microsoft.AspNetCore.Mvc.RazorPages;
								
									namespace
									BlazorCookieAuth.Server.Pages
								
{
								
									[AllowAnonymous]
								
									public
									class
									LoginModel : PageModel
								
									{
								
									public
									string
									ReturnUrl {
									get;
									set; }
								
									public
									async Task<IActionResult>
								
									OnGetAsync(string
									paramUsername,
									string
									paramPassword)
								
									{
								
									string
									returnUrl = Url.Content("~/");
								
									try
								
									{
								
									// Clear the existing external cookie
								
									await HttpContext
								
									.SignOutAsync(
								
									CookieAuthenticationDefaults.AuthenticationScheme);
								
									}
								
									catch
									{ }
								
									// *** !!! This is where you would validate the user !!! ***
								
									// In this example we just log the user in
								
									// (Always log the user in for this demo)
								
									var claims =
									new
									List<Claim>
								
									{
								
									new
									Claim(ClaimTypes.Name, paramUsername),
								
									new
									Claim(ClaimTypes.Role, "Administrator"),
								
									};
								
									var claimsIdentity =
									new
									ClaimsIdentity(
								
									claims, CookieAuthenticationDefaults.AuthenticationScheme);
								
									var authProperties =
									new
									AuthenticationProperties
								
									{
								
									IsPersistent =
									true,
								
									RedirectUri =
									this.Request.Host.Value
								
									};
								
									try
								
									{
								
									await HttpContext.SignInAsync(
								
									CookieAuthenticationDefaults.AuthenticationScheme,
								
									new
									ClaimsPrincipal(claimsIdentity),
								
									authProperties);
								
									}
								
									catch
									(Exception ex)
								
									{
								
									string
									error = ex.Message;
								
									}
								
									return
									LocalRedirect(returnUrl);
								
									}
								
									}
								
}
								

							

Logout.cshtml

								
@page
								
@model BlazorCookieAuth.Server.Pages.LogoutModel
								
@{
								
									ViewData["Title"] = "Logout";
								
}
								
									<
									h2
									>Logout</
									h2
									>
								

							

Logout.cshtml.cs

								
									using
									System;
								
									using
									System.Threading.Tasks;
								
									using
									Microsoft.AspNetCore.Authentication;
								
									using
									Microsoft.AspNetCore.Authentication.Cookies;
								
									using
									Microsoft.AspNetCore.Mvc;
								
									using
									Microsoft.AspNetCore.Mvc.RazorPages;
								
									namespace
									BlazorCookieAuth.Server.Pages
								
{
								
									public
									class
									LogoutModel : PageModel
								
									{
								
									public
									async Task<IActionResult> OnGetAsync()
								
									{
								
									// Clear the existing external cookie
								
									await HttpContext
								
									.SignOutAsync(
								
									CookieAuthenticationDefaults.AuthenticationScheme);
								
									return
									LocalRedirect(Url.Content("~/"));
								
									}
								
									}
								
}
								

							

Add Client Code

image

Add a page called LoginControl.razor to the Shared folder using the following code:

								
@page "/loginControl"
								
@using System.Web;
								
									<
									AuthorizeView
									>
								
									<
									Authorized
									>
								
									<
									b
									>Hello, @context.User.Identity.Name!</
									b
									>
								
									<
									a
									class="ml-md-auto btn btn-primary"
								
									href="/logout?returnUrl=/"
								
									target="_top"
									>Logout</
									a
									>
								
									</
									Authorized
									>
								
									<
									NotAuthorized
									>
								
									<
									input
									type="text"
								
									placeholder="User Name"
								
									@bind="@Username"
									/>
								
									&nbsp;&nbsp;
								
									<
									input
									type="password"
								
									placeholder="Password"
								
									@bind="@Password"
									/>
								
									<
									a
									class="ml-md-auto btn btn-primary"
								
									href="/login?paramUsername=@encode(@Username)&paramPassword=@encode(@Password)"
								
									target="_top"
									>Login</
									a
									>
								
									</
									NotAuthorized
									>
								
									</
									AuthorizeView
									>
								

							
								
									@code
									{
								
									string
									Username = "";
								
									string
									Password = "";
								
									private
									string
									encode(string
									param)
								
									{
								
									return
									HttpUtility.UrlEncode(param);
								
									}
								
}
								

							

							

This code creates a login component that uses the AuthorizeView component to wrap markup code based on the user’s current authentication.

If the user is logged in, we display their name and a Logout button (that navigates the user to the logout page created earlier).

If they are not logged in, we display username and password boxes and a Login button (that navigates the user to the login page created earlier).

image

Finally, we alter the MainLayout.razor page (in the Shared folder) to the following:

								
@inherits LayoutComponentBase
								
									<
									div
									class="sidebar"
									>
								
									<
									NavMenu
									/>
								
									</
									div
									>
								
									<
									div
									class="main"
									>
								
									<
									div
									class="top-row px-4"
									>
								
									<!-- BLAZOR COOKIE Auth Code (begin) -->
								
									<
									LoginControl
									/>
								
									<!-- BLAZOR COOKIE Auth Code (end) -->
								
									</
									div
									>
								
									<
									div
									class="content px-4"
									>
								
									@Body
								
									</
									div
									>
								
									</
									div
									>
								

							

This adds the login component to the top of every page in the Blazor application.

image

Open the App.razor page and surround all the existing code in a CascadingAuthenticationState tag.

image

We can now hit F5 to run the application.

image

We can enter a username and password and click the Login button…

image

We can then look in the Google Chrome Web Browser DevTools and see the cookie has been created.

image

When we click Logout

image

The cookie is removed.

Calling Server Side Controller Methods

At this point all the .razor pages will properly detect if the user is authenticated, and operate as expected. However, if we make a http request to a server side controller, the authenticated user will not be properly detected.

image

To demonstrate this, we first open the startup.cs page and add the following code to the end of the app.UseEndpoints method, (under the endpoints.MapFallbackToPage("/_Host"); line), to allow http requests to controllers to be properly routed:

								
									// ******
								
									// BLAZOR COOKIE Auth Code (begin)
								
									endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
								
									// BLAZOR COOKIE Auth Code (end)
								
									// ******
								

							

image

Next, we create a Controllers folder and add a UserController.cs file with the following code:

								
									using
									Microsoft.AspNetCore.Mvc;
								
									namespace
									BlazorCookieAuth.Controllers
								
{
								
									[Route("api/[controller]")]
								
									[ApiController]
								
									public
									class
									UserController : Controller
								
									{
								
									// /api/User/GetUser
								
									[HttpGet("[action]")]
								
									public
									UserModel GetUser()
								
									{
								
									// Instantiate a UserModel
								
									var userModel =
									new
									UserModel
								
									{
								
									UserName = "[]",
								
									IsAuthenticated =
									false
								
									};
								
									// Detect if the user is authenticated
								
									if
									(User.Identity.IsAuthenticated)
								
									{
								
									// Set the username of the authenticated user
								
									userModel.UserName =
								
									User.Identity.Name;
								
									userModel.IsAuthenticated =
								
									User.Identity.IsAuthenticated;
								
									};
								
									return
									userModel;
								
									}
								
									}
								
									// Class to hold the UserModel
								
									public
									class
									UserModel
								
									{
								
									public
									string
									UserName {
									get;
									set; }
								
									public
									bool
									IsAuthenticated {
									get;
									set; }
								
									}
								
}
								

							

image

We add a new .razor page, CallServerSide.razor, using the following code:

								
@page "/CallServerSide"
								
@using
									BlazorCookieAuth.Controllers
								
@using
									System.Net.Http
								
@inject HttpClient Http
								
@inject NavigationManager UriHelper
								
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor
								
<h3>Call Server Side</h3>
								
<p>Current User: @CurrentUser.UserName</p>
								
<p>IsAuthenticated: @CurrentUser.IsAuthenticated</p>
								
<button
									class="btn btn-primary" @onclick="GetUser">Get User</button>
								
@code {
								
									UserModel CurrentUser =
									new
									UserModel();
								
									async Task GetUser()
								
									{
								
									// Call the server side controller
								
									var url = UriHelper.ToAbsoluteUri("/api/User/GetUser");
								
									var result = await Http.GetJsonAsync<UserModel>(url.ToString());
								
									// Update the result
								
									CurrentUser.UserName = result.UserName;
								
									CurrentUser.IsAuthenticated = result.IsAuthenticated;
								
									}
								
}
								

								

								

							

Finally, we use the following code to add a link to the page in Shared/NavMenu.razor:

								
									<
									li
									class="nav-item px-3"
									>
								
									<
									NavLink
									class="nav-link"
									href="CallServerSide"
									>
								
									<
									span
									class="oi oi-list-rich"
									aria-hidden="true"
									>
									</
									span
									>
									Call Server Side
								
									</
									NavLink
									>
								
									</
									li
									>
								

							

image

We run the application and log in.

image

We navigate to the new Call Server Side control and click the Get User button (that calls the UserController.cs that we just added), and it does not detect the logged in user.

To resolve this, change the GetUser method in the CallServerSide.razor page to the following:

								
									async Task GetUser()
								
									{
								
									// Code courtesy from Oqtane.org (@sbwalker)
								
									// We must pass the authentication cookie in server side requests
								
									var authToken =
								
									HttpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Cookies"];
								
									if
									(authToken !=
									null)
								
									{
								
									Http.DefaultRequestHeaders
								
									.Add("Cookie", ".AspNetCore.Cookies=" + authToken);
								
									// Call the server side controller
								
									var url = UriHelper.ToAbsoluteUri("/api/User/GetUser");
								
									var result = await Http.GetJsonAsync<UserModel>(url.ToString());
								
									// Update the result
								
									CurrentUser.UserName = result.UserName;
								
									CurrentUser.IsAuthenticated = result.IsAuthenticated;
								
									}
								
									}
								

							

We have an authentication cookie, we just need to pass it in the DefaultRequestHeaders.

image

Now, when we log in, and click the Get User button, the controller method is able to detect the logged in user.

Special Thanks

This article would not be possible without sample code (using the full .Net Core Membership provider) provided by SQL-MisterMagoo.

Additional research provided by Shaun Walker.

Links

Blazor.net

Use cookie authentication without ASP.NET Core Identity

ASP.NET Core Blazor authentication and authorization

BlazorTest (SQL-MisterMagoo's site)

Authentication for serverside Blazor (How to use IHttpContextAccessor)

Blazor Security/Authorization

Download

The project is available on the Downloads page on this site.

You must have Visual Studio 2019 (or higher) installed to run the code.

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