You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

653 lines
20 KiB

---
sidebar_position: 3
title: 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](/DemoAuthentication.zip).
## 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.
![required library](/img/authentication/nuget-Microsoft.AspNetCore.Components.Authorization.png)
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.
```csharp title="Models/RegisterRequest.cs"
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; }
}
```
```csharp title="Models/LoginRequest.cs"
public class LoginRequest
{
[Required]
public string Password { get; set; }
[Required]
public string UserName { get; set; }
}
```
```csharp title="Models/CurrentUser.cs"
public class CurrentUser
{
public Dictionary<string, string> Claims { get; set; }
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
}
```
```csharp title="Models/AppUser.cs"
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.
```csharp title="Services/IAuthService.cs"
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.
```csharp title="Services/AuthService.cs"
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.
```csharp title="Services/CustomStateProvider.cs"
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.
```csharp {6-10} title="Program.cs"
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`.
```html title="Shared/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
```html title="Pages/Authentication/Login.razor"
@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>
```
```csharp title="Pages/Authentication/Login.razor.cs"
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
```html title="Pages/Authentication/Register.razor"
@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>
```
```csharp title="Pages/Authentication/Register.razor.cs"
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.
```html title="App.razor"
<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.
```html {11} title="Shared/MainLayout.razor"
@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>
```
```csharp title="Shared/MainLayout.razor.cs"
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.
```html title="Pages/Index.razor"
@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.
```html title="_Imports.razor"
@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:
```html title="Pages/Admin.razor"
@page "/admin"
<h3>Admin Page</h3>
```
We are going to put in our menu our new page:
```html {10-16} title="Shared/NavMenu.razor"
...
<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:
```html
<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:
```html
<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:
```html
<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:
```html
@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:
```html
@page "/"
@attribute [Authorize(Roles = "admin, superuser")]
<p>You can only see this if you're in the 'admin' or 'superuser' role.</p>
```