20 KiB
sidebar_position | title |
---|---|
3 | Authentication |
ASP.NET Core supports configuration and management of security in Blazor apps.
Security scenarios differ between Blazor Server and Blazor WebAssembly apps. Because Blazor Server apps run on the server, permission checks can determine the following:
- User interface options presented to a user (for example, menu entries available to a user).
- Access rules for application areas and components.
Blazor WebAssembly apps run on the client. Authorization is only used to determine which UI options to display. Because client-side controls can be modified or overridden by a user, a Blazor WebAssembly app can't enforce authorization access rules.
For our examples we will use a small example project available here.
Creating a client application
We are going to create a client application to manage local authentication.
Create a new Blazor WASM app.
Install the Microsoft.AspNetCore.Components.Authorization
package in version 5.0.13.
Or using the Package Manager console: PM> Install-Package Microsoft.AspNetCore.Components.Authorization -Version 5.0.13
:::caution
In Blazor WebAssembly apps, authentication checks can be skipped because all client-side code can be modified by users. This also applies to all client-side application technologies, including JavaScript SPA application frameworks or native applications for any operating system.
:::
Models
As usual, we need to create the model classes that would take various authentication parameters for login and registration of new users.
We will create these classes in the Models
folder.
public class RegisterRequest
{
[Required]
public string Password { get; set; }
[Required]
[Compare(nameof(Password), ErrorMessage = "Passwords do not match!")]
public string PasswordConfirm { get; set; }
[Required]
public string UserName { get; set; }
}
public class LoginRequest
{
[Required]
public string Password { get; set; }
[Required]
public string UserName { get; set; }
}
public class CurrentUser
{
public Dictionary<string, string> Claims { get; set; }
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
}
public class AppUser
{
public string Password { get; set; }
public List<string> Roles { get; set; }
public string UserName { get; set; }
}
We now have classes to help persist authentication settings.
Creating the authentication service
We will now create our authentication service, this one will use a list of users in memory, only the user Admin
will be defined by default.
Add a new interface in the project.
public interface IAuthService
{
CurrentUser GetUser(string userName);
void Login(LoginRequest loginRequest);
void Register(RegisterRequest registerRequest);
}
Let's add a concrete class and implement the previous interface.
public class AuthService : IAuthService
{
private static readonly List<AppUser> CurrentUser;
static AuthService()
{
CurrentUser = new List<AppUser>
{
new AppUser { UserName = "Admin", Password = "123456", Roles = new List<string> { "admin" } }
};
}
public CurrentUser GetUser(string userName)
{
var user = CurrentUser.FirstOrDefault(w => w.UserName == userName);
if (user == null)
{
throw new Exception("User name or password invalid !");
}
var claims = new List<Claim>();
claims.AddRange(user.Roles.Select(s => new Claim(ClaimTypes.Role, s)));
return new CurrentUser
{
IsAuthenticated = true,
UserName = user.UserName,
Claims = claims.ToDictionary(c => c.Type, c => c.Value)
};
}
public void Login(LoginRequest loginRequest)
{
var user = CurrentUser.FirstOrDefault(w => w.UserName == loginRequest.UserName && w.Password == loginRequest.Password);
if (user == null)
{
throw new Exception("User name or password invalid !");
}
}
public void Register(RegisterRequest registerRequest)
{
CurrentUser.Add(new AppUser { UserName = registerRequest.UserName, Password = registerRequest.Password, Roles = new List<string> { "guest" } });
}
}
Authentication State Provider
As the name suggests, this class provides the authentication state of the user in Blazor Applications.
AuthenticationStateProvider
is an abstract class in the Authorization
namespace.
Blazor uses this class which will be inherited and replaced by us with a custom implementation to get user state.
This state can come from session storage, cookies or local storage as in our case.
Let's start by adding the Provider class to the Services
folder. Let's call it CustomStateProvider
. As mentioned, this class will inherit from the AuthenticationStateProvider
class.
public class CustomStateProvider : AuthenticationStateProvider
{
private readonly IAuthService _authService;
private CurrentUser _currentUser;
public CustomStateProvider(IAuthService authService)
{
this._authService = authService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
try
{
var userInfo = GetCurrentUser();
if (userInfo.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, _currentUser.UserName) }.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex);
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public async Task Login(LoginRequest loginParameters)
{
_authService.Login(loginParameters);
// No error - Login the user
var user = _authService.GetUser(loginParameters.UserName);
_currentUser = user;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Logout()
{
_currentUser = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Register(RegisterRequest registerParameters)
{
_authService.Register(registerParameters);
// No error - Login the user
var user = _authService.GetUser(registerParameters.UserName);
_currentUser = user;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private CurrentUser GetCurrentUser()
{
if (_currentUser != null && _currentUser.IsAuthenticated)
{
return _currentUser;
}
return new CurrentUser();
}
}
You can see that, by default, we manage to implement a GetAuthenticationStateAsync
function.
Now, this function is quite important because Blazor calls it very often to check the authentication status of the user in the application.
Additionally, we will inject the AuthService
service and the CurrentUser
model to the constructor of this class.
The idea behind this is that we will not directly use the service instance in the view (Razor components), rather we will inject the CustomStateProvider
instance in our views which will in turn access the services.
Explanation
GetAuthenticationStateAsync
- Get the current user from the service object. if the user is authenticated, we will add their claims to a list and create a claims identity. After that, we will return an authentication state with the required data.
The other 3 methods are quite simple. We will simply call the required service methods. But here is one more thing I would like to explain.
Now, with each connection, registration, disconnection, there is technically a change of state in the authentication.
We need to notify the entire application that the user's state has changed.
Therefore, we use a notify method and pass the current authentication state by calling GetAuthenticationStateAsync
. Pretty logical, huh?
Now, to enable these services and dependencies in the project, we need to register them in the IOC, right?
To do this, navigate to the project's Program.cs
and make the following additions.
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomStateProvider>());
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
Now let's work on adding the Razor components for the UI. Here we are going to add 2 components i.e. Login component and Register component. We will protect the entire application by making it secure. This means that only an authenticated user will be allowed to view page data. Thus, we will have a separate layout component for the login/registration components.
Go to the Shared
folder of the project.
Here is where you should ideally add all shared Razor components.
In our case, let's add a new Razor component and call it, AuthLayout.razor
.
@inherits LayoutComponentBase
<div class="main">
<div class="content px-4">
@Body
</div>
</div>
You can see it's a pretty basic html file with an inheritance tag. We won't focus on css/html. However, this layout component will act as a container that will hold the login and register component itself.
Login Component
@page "/login"
@layout AuthLayout
<h1 class="h2 font-weight-normal login-title">
Login
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="loginRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" @bind-Value="loginRequest.UserName" autofocus placeholder="Username" />
<ValidationMessage For="@(() => loginRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="loginRequest.Password" />
<ValidationMessage For="@(() => loginRequest.Password)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<label class="text-danger">@error</label>
<NavLink href="register">
<h6 class="font-weight-normal text-center">Create account</h6>
</NavLink>
</EditForm>
public partial class Login
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private string error { get; set; }
private LoginRequest loginRequest { get; set; } = new LoginRequest();
private async Task OnSubmit()
{
error = null;
try
{
await AuthStateProvider.Login(loginRequest);
NavigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}
Register Component
@page "/register"
@layout AuthLayout
<h1 class="h2 font-weight-normal login-title">
Register
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="registerRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" placeholder="Username" autofocus @bind-Value="@registerRequest.UserName" />
<ValidationMessage For="@(() => registerRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="@registerRequest.Password" />
<ValidationMessage For="@(() => registerRequest.Password)" />
<label for="inputPasswordConfirm" class="sr-only">Password Confirmation</label>
<InputText type="password" id="inputPasswordConfirm" class="form-control" placeholder="Password Confirmation" @bind-Value="@registerRequest.PasswordConfirm" />
<ValidationMessage For="@(() => registerRequest.PasswordConfirm)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Create account</button>
<label class="text-danger">@error</label>
<NavLink href="login">
<h6 class="font-weight-normal text-center">Already have an account? Click here to login</h6>
</NavLink>
</EditForm>
public partial class Register
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private string error { get; set; }
private RegisterRequest registerRequest { get; set; } = new RegisterRequest();
private async Task OnSubmit()
{
error = null;
try
{
await AuthStateProvider.Register(registerRequest);
NavigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}
Enable authentication
Now we need to let Blazor know that authentication is enabled and we need to pass the Authorize
attribute throughout the application.
To do this, modify the main component, i.e. the App.razor
component.
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>
What is AuthorizeRoteView
?
It is a combination of AuthorizeView
and RouteView
, so it shows the specific page but only to authorized users.
What is CascadingAuthenticationState
?
This provides a cascading parameter to all descendant components.
Let's add a logout button to the top navigation bar of the app to allow the user to logout as well as force redirect to the Login
page if the user is not logged in.
We will need to make changes to the MainLayout.razor
component for this.
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
<button type="button" class="btn btn-link ml-md-auto" @onclick="@LogoutClick">Logout</button>
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
public partial class MainLayout
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[CascadingParameter]
private Task<AuthenticationState> AuthenticationState { get; set; }
protected override async Task OnParametersSetAsync()
{
if (!(await AuthenticationState).User.Identity.IsAuthenticated)
{
NavigationManager.NavigateTo("/login");
}
}
private async Task LogoutClick()
{
await AuthStateProvider.Logout();
NavigationManager.NavigateTo("/login");
}
}
We want to display our username and these roles on the index page soon after we log in.
Go to Pages/Index.razor
and make the following changes.
@page "/"
<AuthorizeView>
<Authorized>
<h1>Hello @context.User.Identity.Name !!</h1>
<p>Welcome to Blazor Learner.</p>
<ul>
@foreach (var claim in context.User.Claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
</Authorized>
<Authorizing>
<h1>Loading ...</h1>
</Authorizing>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
You can see there's a bunch of unsolvable errors that probably indicate few things are undefined in the namespace or something.
This is because we didn't import the new namespaces.
To do this, navigate to _Import.razor
and add these lines at the bottom.
@using Microsoft.AspNetCore.Components.Authorization
Similarly we want to create an administration page accessible only by users with the admin
role.
Create the Pages/Admin.razor
page:
@page "/admin"
<h3>Admin Page</h3>
We are going to put in our menu our new page:
...
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<AuthorizeView Roles="admin">
<li class="nav-item px-3">
<NavLink class="nav-link" href="admin" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Admin page
</NavLink>
</li>
</AuthorizeView>
</ul>
</div>
...
Concept: Composant AuthorizeView
The AuthorizeView
component displays UI content selectively, depending on whether the user is authorized or not.
This approach is useful when you only need to display user data and don't need to use the user's identity in procedural logic.
The component exposes a context variable of type AuthenticationState
, which you can use to access information about the logged-in user:
<AuthorizeView>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authenticated.</p>
</AuthorizeView>
You can also provide different content for display if the user is not authorized:
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authorized.</p>
<button @onclick="SecureMethod">Authorized Only Button</button>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
@code {
private void SecureMethod() { ... }
}
The content of the <Authorized>
and <NotAuthorized>
tags can include arbitrary elements, such as other interactive components.
A default event handler for an authorized element, such as the SecureMethod
method of the <button>
element in the preceding example, can be called only by an authorized user.
Authorization requirements, such as roles or policies that control user interface or access options, are covered in the Authorization
section.
If authorization conditions are not specified, AuthorizeView
uses a default policy and processes:
- Users authenticated (logged in) as authorized.
- Unauthenticated (logged out) users as unauthorized.
Concept: Authorization based on role and policy
The AuthorizeView
component supports both role-based and policy-based authorization.
For role-based authorization, use the Roles
parameter:
<AuthorizeView Roles="admin, superuser">
<p>You can only see this if you're an admin or superuser.</p>
</AuthorizeView>
Concept: Attribut [Authorize]
The [Authorize] attribute can be used in Razor components:
@page "/"
@attribute [Authorize]
You can only see this if you're signed in.
:::caution
Only use [Authorize]
on @page
components reached through the Blazor router.
Authorization is done only as an aspect of routing and not for child components rendered in a page.
To authorize viewing specific items in a page, use AuthorizeView
instead.
:::
The [Authorize] attribute also supports role-based or policy-based authorization. For role-based authorization, use the Roles
parameter:
@page "/"
@attribute [Authorize(Roles = "admin, superuser")]
<p>You can only see this if you're in the 'admin' or 'superuser' role.</p>