Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App
This guide provides detailed steps to implement a basic username and password authentication system in a .NET 9 MAUI Blazor Hybrid app using the new Blazor Web App template with a .Shared
project. The authentication system uses a simple in-memory user store for demonstration purposes.
Table of Contents
- Project Structure Overview
- Prerequisites
- Step-by-Step Implementation
- Additional Considerations
- Conclusion
Project Structure Overview
Your solution will consist of three projects:
YourApp.Web
: The Blazor Web App project.YourApp.Mobile
: The MAUI Blazor Hybrid project.YourApp.Shared
: Contains shared components, pages, and services.
Prerequisites
- .NET 9 SDK: Ensure that you have the .NET 9 SDK installed.
- Visual Studio 2022 (17.7 or later): For project creation and development.
- MAUI Workload: Install the MAUI workload for cross-platform development.
- Install the following Nuget Packages
-
- In the .SharedProject
- Microsoft.AspNetCore.Authorization (9.0.0)
- Microsoft.AspNetCore.Components.Authorization (9.0.0)
- Microsoft.AspNetCore.Components.Web (9.0.0)
- Microsoft.Maui.Essentials (9.0.10)
- In the .SharedProject
-
Step-by-Step Implementation
Create a New Solution
- Open Visual Studio 2022.
- Create a New Project:
- Template: Search for Blazor Web App.
- Project Name: Enter
YourApp.Web
. - Solution Name: Enter
YourApp
.
- Add a MAUI Blazor App:
- Right-click on the solution in Solution Explorer.
- Select Add > New Project.
- Choose .NET MAUI Blazor App.
- Project Name: Enter
YourApp.Mobile
.
- Add a Class Library for Shared Code:
- Right-click on the solution.
- Select Add > New Project.
- Choose Class Library.
- Project Name: Enter
YourApp.Shared
.
- Add References:
- In
YourApp.Web
andYourApp.Mobile
, add a reference toYourApp.Shared
.
- In
- Set Up the Projects:
- Ensure all projects target .NET 9.
- Update the
.csproj
files if necessary.
Add some global usings for razor pages
In YourApp.Shared
, open and edit _Imports.razor
and add the following usings:
@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization; @using YourApp.Shared.Layout @using YourApp.Shared.Pages @using YourApp.Shared.Services
Implement the Authentication State Provider
Create CustomAuthenticationStateProvider
In YourApp.Shared
, create a new folder called Services
and add the following class:
// YourApp.Shared/Services/CustomAuthenticationStateProvider.cs
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;
namespace YourApp.Shared.Services
{
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
private ClaimsPrincipal _currentUser;
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return Task.FromResult(new AuthenticationState(_currentUser ?? _anonymous));
}
public void SignIn(string username)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}, "apiauth_type");
_currentUser = new ClaimsPrincipal(identity);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public void SignOut()
{
_currentUser = _anonymous;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}
}
Register the Authentication State Provider
In YourApp.Web/Program.cs
:
using YourApp.Shared.Services;
using Microsoft.AspNetCore.Components.Authorization;
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider =>
provider.GetRequiredService<CustomAuthenticationStateProvider>());
builder.Services.AddAuthorizationCore();
var app = builder.Build();
// Configure middleware
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
In YourApp.Mobile/MauiProgram.cs
:
using YourApp.Shared.Services;
using Microsoft.AspNetCore.Components.Authorization;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Register services
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
#endif
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider =>
provider.GetRequiredService<CustomAuthenticationStateProvider>());
builder.Services.AddAuthorizationCore();
return builder.Build();
}
}
Implement a Simple User Store
Create UserService.cs
in YourApp.Shared/Services
:
// YourApp.Shared/Services/UserService.cs
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace YourApp.Shared.Services
{
public class UserService
{
private List<UserCredential> _users = new List<UserCredential>
{
new UserCredential { Username = "user1", Password = "password1", Role = "User" },
new UserCredential { Username = "admin", Password = "admin123", Role = "Admin" }
};
public Task<UserCredential> AuthenticateUserAsync(string username, string password)
{
var user = _users.FirstOrDefault(u => u.Username == username && u.Password == password);
return Task.FromResult(user);
}
}
public class UserCredential
{
public string Username { get; set; }
public string Password { get; set; }
public string Role { get; set; }
}
}
Register UserService
in both Program.cs
and MauiProgram.cs
:
builder.Services.AddScoped<UserService>();
Build Login and Logout Components
Create Login.razor
In YourApp.Shared/Pages
:
@page "/login"
@using YourApp.Shared.Services
@inject CustomAuthenticationStateProvider AuthStateProvider
@inject UserService UserService
@inject NavigationManager NavigationManager
<h3>Login</h3>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-danger">@ErrorMessage</div>
}
<EditForm Model="loginModel" OnValidSubmit="HandleLogin">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>Username:</label>
<InputText @bind-Value="loginModel.Username" />
</div>
<div>
<label>Password:</label>
<InputText type="password" @bind-Value="loginModel.Password" />
</div>
<button type="submit">Login</button>
</EditForm>
@code {
private LoginModel loginModel = new LoginModel();
private string ErrorMessage;
private async Task HandleLogin()
{
var user = await UserService.AuthenticateUserAsync(loginModel.Username, loginModel.Password);
if (user != null)
{
AuthStateProvider.SignIn(user.Username);
NavigationManager.NavigateTo("/");
}
else
{
ErrorMessage = "Invalid username or password.";
}
}
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}
Create Logout.razor
In YourApp.Shared/Pages
:
@page "/logout"
@inject CustomAuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
<h3>Logging out...</h3>
@code {
protected override void OnInitialized()
{
AuthStateProvider.SignOut();
NavigationManager.NavigateTo("/");
}
}
Protect Pages with Authorization
Create a Protected Page
Create ProtectedPage.razor
in YourApp.Shared/Pages
:
@page "/protected"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider
<h3>Protected Page</h3>
<p>Welcome, @userName!</p>
@code {
private string userName;
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
userName = user.Identity?.Name;
}
}
Ensure Required Packages Are Installed
Add the following NuGet packages to YourApp.Shared
:
- Microsoft.AspNetCore.Authorization
- Microsoft.AspNetCore.Components.Authorization
Update the Navigation Menu
In YourApp.Shared/Shared
, update NavMenu.razor
:
@inject AuthenticationStateProvider AuthenticationStateProvider
@implements IDisposable
<nav>
<ul>
<li><NavLink href="/">Home</NavLink></li>
@if (isAuthenticated)
{
<li><NavLink href="protected">Protected Page</NavLink></li>
<li><NavLink href="logout">Logout (@userName)</NavLink></li>
}
else
{
<li><NavLink href="login">Login</NavLink></li>
}
</ul>
</nav>
@code {
private bool isAuthenticated;
private string userName;
private AuthenticationState authenticationState;
protected override async Task OnInitializedAsync()
{
authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
UpdateAuthenticationState(authenticationState);
AuthenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
}
private void UpdateAuthenticationState(AuthenticationState authState)
{
var user = authState.User;
isAuthenticated = user.Identity?.IsAuthenticated ?? false;
userName = user.Identity?.Name;
StateHasChanged();
}
private void OnAuthenticationStateChanged(Task<AuthenticationState> task)
{
InvokeAsync(async () =>
{
var authState = await task;
UpdateAuthenticationState(authState);
});
}
public void Dispose()
{
AuthenticationStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
}
}
Adjust Routing in Routes.razor
In YourApp.Shared
, update Routes.razor
:
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Routing
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Routes).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity.IsAuthenticated)
{
<p>You are not authorized to access this resource.</p>
}
else
{
<RedirectToLogin />
}
</NotAuthorized>
<Authorizing>
<p>Authorizing...</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
private AuthenticationState context => authenticationStateTask.Result;
}
Create RedirectToLogin.razor
In YourApp.Shared/Components
:
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
var returnUrl = Uri.EscapeDataString(NavigationManager.Uri);
NavigationManager.NavigateTo($"/login?returnUrl={returnUrl}");
}
}
Test the Application
Run the Web App
- Start the Web Project (
YourApp.Web
). - Navigate to a Protected Page: Visit
/protected
. - Verify Redirection: You should be redirected to the login page.
- Log In: Use
user1
/password1
. - Access Protected Content: You should now see the protected page.
Run the Mobile App
- Start the Mobile Project (
YourApp.Mobile
). - Repeat the Steps: Perform the same steps as above to verify authentication on mobile.
Additional Considerations
Persisting Authentication State
To persist authentication state across sessions, modify the CustomAuthenticationStateProvider
to use secure storage.
Implementing Secure Storage
Update CustomAuthenticationStateProvider.cs
:
using Microsoft.JSInterop;
using System.Text.Json;
using Microsoft.Maui.Storage;
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly IJSRuntime _jsRuntime;
private const string _authDataKey = "authData";
private ClaimsPrincipal _currentUser;
public CustomAuthenticationStateProvider(IJSRuntime jsRuntime = null)
{
_jsRuntime = jsRuntime;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (_currentUser != null)
{
return new AuthenticationState(_currentUser);
}
string authDataJson = null;
#if ANDROID || IOS || WINDOWS
authDataJson = await SecureStorage.Default.GetAsync(_authDataKey);
#else
if (_jsRuntime != null)
{
authDataJson = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", _authDataKey);
}
#endif
if (!string.IsNullOrWhiteSpace(authDataJson))
{
var authData = JsonSerializer.Deserialize<AuthData>(authDataJson);
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, authData.Username)
}, "apiauth_type");
_currentUser = new ClaimsPrincipal(identity);
}
else
{
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
}
return new AuthenticationState(_currentUser);
}
public async void SignIn(string username)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}, "apiauth_type");
_currentUser = new ClaimsPrincipal(identity);
var authData = new AuthData { Username = username };
var authDataJson = JsonSerializer.Serialize(authData);
#if ANDROID || IOS || WINDOWS
await SecureStorage.Default.SetAsync(_authDataKey, authDataJson);
#else
if (_jsRuntime != null)
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", _authDataKey, authDataJson);
}
#endif
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async void SignOut()
{
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
#if ANDROID || IOS || WINDOWS
SecureStorage.Default.Remove(_authDataKey);
#else
if (_jsRuntime != null)
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", _authDataKey);
}
#endif
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private class AuthData
{
public string Username { get; set; }
}
}
Enhancing Security
- Password Hashing: Implement hashing for passwords instead of storing them in plain text.
- Use HTTPS: Ensure your web application uses HTTPS to encrypt data in transit.
- Input Validation: Validate all user inputs to prevent injection attacks.
Conclusion
By following these steps, you’ve successfully implemented a basic authentication system in your .NET 9 MAUI Blazor Hybrid app using the new Blazor Web App template with a .Shared
project. This setup allows you to:
- Understand the Basics: Gain insights into how authentication works in Blazor and MAUI.
- Share Code: Use the
.Shared
project to share components and services between web and mobile apps. - Prepare for Future Enhancements: Lay the groundwork for integrating more secure and complex authentication mechanisms in the future.
Next Steps:
- Implement Secure Authentication: Consider integrating OAuth or OpenID Connect for robust authentication.
- Role-Based Authorization: Utilize roles to manage user access to different parts of the application.
- UI/UX Improvements: Enhance the user interface for better usability and aesthetics.
Feel free to customize this guide further or reach out if you have any questions or need additional assistance!