Today marks the commencement of our exploration into authentication within Blazor Server and ASP.NET Core. Our journey will begin with a rudimentary example centered around cookies, gradually progressing towards the development of a comprehensive application utilizing the SAML protocol. Our overarching aim is to illuminate the complexities and underlying mechanisms of the authentication process in ASP.NET Core.
Introduction
Authentication stands as a paramount consideration in every application. Consequently, ASP.NET Core inherently furnishes mechanisms to streamline this process, empowering us to tailor it to our specific needs and requirements.
Regrettably, this crucial process is frequently undervalued by stakeholders throughout the development lifecycle, primarily because developers tend to prioritize business needs and strive to fulfill requirements. Moreover, documentation on this topic is often fragmented or narrowly focuses on specific needs. In this series, our objective is thus to provide a comprehensive overview of authentication in ASP.NET, offering a coherent understanding of its intricacies and significance.
The subsequent textbooks prove useful for concluding this series.
Blazor in Action (Sainty)
Pro ASP.NET Core Identity (Freeman)
ASP.NET Core in Action (Lock)
This post was originally posted here: Understanding authentication in Blazor and ASP.NET Core
What is authentication ?
Authentication is the process of verifying the identity of a user or system attempting to access a resource or service. It ensures that the entity requesting access is who they claim to be. This verification typically involves presenting credentials, such as a username and password, and validating them against a known set of credentials stored securely. Authentication mechanisms help secure systems and protect sensitive information by ensuring that only authorized users can access protected resources.
We will refrain from delving deeper into this topic as the reader is already familiar with its details.
What is Blazor ?
Blazor is a framework for building interactive web applications using C# and .NET. It allows developers to create web applications entirely in C# without needing to rely on JavaScript for client-side functionality (like React or Angular). Once more, we will abstain from further elaboration on this subject and encourage the reader to explore dedicated books or blogs for more advanced concepts.
Information
To be frank, we are not Blazor experts. Our focus here is solely on exploring the authentication capabilities provided by this framework.
Establishing the environment
Now, we will proceed to configure a standard Blazor environment within the Visual Studio 2022 IDE. This basic application will serve as our foundation for gradually elucidating the underlying concepts.
- Create a new solution named EOCS.BlazorAuthentication for example and a new Blazor Web App project in it named EOCS.BlazorAuthentication.Main.
- When adding information, ensure to select "None" as the Authentication type, "Server" for the interactive render mode and to check "Include sample pages".
- Run the program and verify that it is possible to access all routes without the requirement for authentication.
Information
In this series, we are utilizing the .NET 8 version of the .NET framework.
Our objective now is to demonstrate how to seamlessly integrate authentication into this application, ensuring that only authenticated users can access its various pages.
Adding authentication
Incorporating authentication entails creating a login page where users can authenticate their identity by providing credentials. Additionally, it involves specifying that other pages cannot be accessed unless the provided credentials are verified as correct. Let's explore these two aspects in detail.
Creating a login page
- Add a new Razor component named Login.razor to the application and add the following code in it.
@page "/login"
<h3>Login</h3>
<form action="" method="post">
<button type="submit" class="btn btn-primary">Login</button>
</form>
We can readily observe that a new page has been seamlessly integrated into our application, accessible via the /login URL.
Specifying that other pages cannot be accessed
Up to this point, all existing routes remain publicly accessible. We will now enforce access restrictions to ensure that only authenticated users can view our pages.
- Open the Routes.razor file, add the [Authorize] attribute to it and edit the content with the following code.
@inject NavigationManager navigationManager
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
<NotAuthorized>
@{
navigationManager.NavigateTo("/login", true);
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
@{
navigationManager.NavigateTo("/login", true);
}
</NotFound>
</Router>
Information
In previous versions of the framework, the aforementioned configurations may need to be adjusted, especially if the Routes.razor file does not exist.
Once this setup is established, every file requiring authentication must be annotated with the Authorize attribute.
@page "/"
@attribute [Authorize]
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
Information
To ensure that the Authorize attribute is recognized, verify that the correct namespaces are imported in the _Imports.razor file.
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
There are several noteworthy aspects in this code that deserve attention:
-
Files that should be accessible only to authenticated users only must be globally adorned with the [Authorize] attribute. It signifies that by default, authentication is enabled and must be validated when the page is loaded.
-
We are employing the AuthorizeRouteView component in the Routes.cs class to define the behavior when a page is requested: in our scenario, any unauthenticated user should be redirected to the login page, while authenticated users are directed to the requested page.
-
It's worth noting that if a malicious user attempts to access a URL that does exist, they will also be redirected to the login page.
However, upon running the program, an error occurs. What could be causing this discrepancy ?
In reality, we haven't informed Blazor that the authentication feature must be utilized in our application.
Important
We must acknowledge that Microsoft modifies the configuration settings of an ASP.NET application with each new release of the framework, which can indeed be quite tedious. Is it not feasible for them to maintain stable Program.cs and Startup.cs classes ?
For this article, we are reliant on .NET 8.
What is the operational mechanism of authentication in ASP.NET Core ?
Authentication in ASP.NET Core or Blazor is undeniably a complex subject, and our aim is to present it in the most comprehensible manner possible. Additionally, it's important to note that certain differences may arise based on the configuration chosen at the startup of the solution.
How does ASP.NET discern the necessity for authentication ?
The answer is rather straightforward: we designate pages requiring authentication by adorning them with the [Authorize] attribute, and indicate in the Routes.cs class that certain routes should be authenticated with the AuthorizeRouteView tag.
@page "/"
@attribute [Authorize]
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
<NotAuthorized>
@{
navigationManager.NavigateTo("/login", true);
}
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
@{
navigationManager.NavigateTo("/login", true);
}
</NotFound>
</Router>
These configurations will instruct ASP.NET to employ a mechanism for verifying the identity of the current user through an authentication process. However, which process does it choose ? There are numerous possibilities: users can be authenticated using cookies, JWT tokens, the SAML protocol, or other methods. Once again, the answer is rather straightforward: ASP.NET selects the method that we configure !
Important
A bit of terminology: an authentication process in ASP.NET Core is referred to as an authentication scheme in Microsoft jargon.
Therefore, we need to define one or more authentication schemes (it is perfectly acceptable to offer users multiple login options). The good news is that Microsoft already provides native schemes and simplifies the development of custom ones.
Here, for example, we configure our application to require authentication when a cookie named "Auth" is present in the user's browser. If this cookie is not present, users are redirected to the login page. We didn't need to write complex or convoluted code to fetch and decrypt the cookie because ASP.NET Core provides this functionality natively.
The equivalent code in C# is as follows (add it to the Program.cs file).
public static void Main(string[] args)
{
builder.Services.AddAuthentication("Auth")
.AddCookie("Auth", options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
options.LoginPath = "/login";
});
builder.Services.AddCascadingAuthenticationState();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();
}
Notice in this code snippet how :
- we define an authentication scheme named "Auth" based on cookies
- we designate it as the default authentication scheme
Configured in this manner, the ASP.NET engine comprehends that authentication is required, and that it can be executed using a cookie in a browser.
At this stage, of course, we are redirected to the login page as the cookie does not exist. Now, we need to establish a method to create it.
Finalizing the authentication process
How to login ?
Here, we will construct an oversimplified login page to authenticate a user. In a real-world scenario, we would need to retrieve data from a datastore to verify the accuracy of the credentials. However, for illustrative purposes, we will create the necessary cookie directly without performing any verification.
The Login button triggers a POST method, so we will create a controller that responds to this request and generates the required cookie within it.
-
Add a new folder in the project, name it Authentication and add a controller named AuthController within it.
-
Add the following code in AuthController.cs.
public class AuthController : Controller
{
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> CookieLogin()
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, "John Patton"));
claims.Add(new Claim(ClaimTypes.Role, "Contributor"));
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Auth"));
await HttpContext.SignInAsync("Auth", principal).ConfigureAwait(false);
return Redirect("/");
}
}
Information
The SignInAsync method of HttpContext is a native method that allows us to sign in a principal for the default authentication scheme.
In our case, since the authentication scheme relies on cookies, this method will handle the creation of the cookie for us, considering the underlying code for encryption, expiration and so forth.
- Edit the Login.razor class.
@page "/login"
<form action="Auth/CookieLogin" method="post">
<button type="submit" class="btn btn-primary">Login</button>
</form>
- Edit also the Program.cs class.
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddAuthentication("Auth")
.AddCookie("Auth", options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
options.LoginPath = "/login";
});
builder.Services.AddCascadingAuthenticationState();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller}/{action}");
});
app.Run();
}
Now, when we click on Login, a cookie is generated, and we can navigate within the application.
How to logout ?
Similarly, we can implement the logout process by invoking the SignOutAsync method of HttpContext.
public class AuthController : Controller
{
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> CookieLogin()
{
}
[HttpPost]
public async Task<IActionResult> CookieLogout()
{
await HttpContext.SignOutAsync("Auth").ConfigureAwait(false);
return Redirect("/login");
}
}
The logic presented here was rather basic, and now we will delve into a more intricate topic with authentication based on the SAML protocol. But to avoid overloading this article, readers interested in this implementation can find the continuation here.