Software – GeoSaffer.com https://blog.geosaffer.com Apps, Electronics, 3D Printing & more Mon, 18 Nov 2024 23:36:28 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.1 179389722 Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App (SSR) https://blog.geosaffer.com/2024/11/20/187/?utm_source=rss&utm_medium=rss&utm_campaign=187 https://blog.geosaffer.com/2024/11/20/187/#respond Wed, 20 Nov 2024 08:00:12 +0000 https://blog.geosaffer.com/?p=187

Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App (SSR)

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 accounts for the limitations of server-side rendering (SSR) in Blazor Server applications and uses platform-specific authentication services.


Table of Contents

  1. Project Structure Overview
  2. Prerequisites
  3. Step-by-Step Implementation
  4. Additional Considerations
  5. Conclusion

Project Structure Overview

Your solution will consist of three projects:

  • YourApp.Web: The Blazor Server (Web) application.
  • YourApp.Mobile: The MAUI Blazor Hybrid (Mobile) application.
  • 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.12 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)

Step-by-Step Implementation

Create a New Solution

  1. Open Visual Studio 2022.
  2. Create a New Project:
    • Template: Search for Blazor Web App.
    • Project Name: Enter YourApp.Web.
    • Solution Name: Enter YourApp.
  3. 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.
  4. Add a Class Library for Shared Code:
    • Right-click on the solution.
    • Select Add > New Project.
    • Choose Class Library.
    • Project Name: Enter YourApp.Shared.
  5. Add References:
    • In YourApp.Web and YourApp.Mobile, add a reference to YourApp.Shared.
  6. 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 Platform-Specific Authentication Services

Due to the limitations of server-side rendering (SSR) in Blazor Server applications, we’ll implement platform-specific authentication services to handle authentication differently in the web and mobile applications.

Create an Interface for Authentication

In YourApp.Shared/Services, create ICustomAuthenticationService.cs:


// YourApp.Shared/Services/ICustomAuthenticationService.cs
using System.Security.Claims;
using System.Threading.Tasks;

namespace YourApp.Shared.Services
{
    public interface ICustomAuthenticationService
    {
        Task SignInAsync(string username);
        Task SignOutAsync();
        Task<ClaimsPrincipal> GetCurrentUserAsync();
    }
}

Implement the Authentication Service for Blazor Server

In YourApp.Web/Services, create ServerAuthenticationService.cs:


// YourApp.Web/Services/ServerAuthenticationService.cs
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using YourApp.Shared.Services;

namespace YourApp.Web.Services
{
    public class ServerAuthenticationService : ICustomAuthenticationService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public ServerAuthenticationService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public async Task SignInAsync(string username)
        {
            var claims = new[] { new Claim(ClaimTypes.Name, username) };
            var identity = new ClaimsIdentity(claims, "Cookies");
            var principal = new ClaimsPrincipal(identity);

            await _httpContextAccessor.HttpContext.SignInAsync("Cookies", principal);
        }

        public async Task SignOutAsync()
        {
            await _httpContextAccessor.HttpContext.SignOutAsync("Cookies");
        }

        public Task<ClaimsPrincipal> GetCurrentUserAsync()
        {
            var user = _httpContextAccessor.HttpContext.User;
            return Task.FromResult(user);
        }
    }
}

Update Program.cs in the Web Project

Configure authentication services in YourApp.Web/Program.cs:


// YourApp.Web/Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using YourApp.Shared.Services;
using YourApp.Web.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

// Configure cookie authentication
builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", options =>
    {
        options.LoginPath = "/login";
    });

builder.Services.AddAuthorizationCore();

// Register IHttpContextAccessor
builder.Services.AddHttpContextAccessor();

// Register the server-specific authentication service
builder.Services.AddScoped<ICustomAuthenticationService, ServerAuthenticationService>();

// Register the custom AuthenticationStateProvider
builder.Services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();

// Register UserService
builder.Services.AddScoped<UserService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseStaticFiles();
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

Create ServerAuthenticationStateProvider

In YourApp.Web/Services, create ServerAuthenticationStateProvider.cs:


// YourApp.Web/Services/ServerAuthenticationStateProvider.cs
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;
using YourApp.Shared.Services;

namespace YourApp.Web.Services
{
    public class ServerAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly ICustomAuthenticationService _authenticationService;

        public ServerAuthenticationStateProvider(ICustomAuthenticationService authenticationService)
        {
            _authenticationService = authenticationService;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var user = await _authenticationService.GetCurrentUserAsync();
            return new AuthenticationState(user);
        }
    }
}

Implement the Authentication Service for MAUI

In YourApp.Shared/Services, create CustomAuthenticationService.cs:


// YourApp.Shared/Services/CustomAuthenticationService.cs
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

namespace YourApp.Shared.Services
{
    public class CustomAuthenticationService : ICustomAuthenticationService
    {
        private readonly CustomAuthenticationStateProvider _authStateProvider;

        public CustomAuthenticationService(CustomAuthenticationStateProvider authStateProvider)
        {
            _authStateProvider = authStateProvider;
        }

        public async Task SignInAsync(string username)
        {
            await _authStateProvider.SignInAsync(username);
        }

        public async Task SignOutAsync()
        {
            await _authStateProvider.SignOutAsync();
        }

        public async Task<ClaimsPrincipal> GetCurrentUserAsync()
        {
            var authState = await _authStateProvider.GetAuthenticationStateAsync();
            return authState.User;
        }
    }
}

Adjust CustomAuthenticationStateProvider for MAUI

In YourApp.Shared/Services, update CustomAuthenticationStateProvider.cs:


// YourApp.Shared/Services/CustomAuthenticationStateProvider.cs
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Maui.Storage;

namespace YourApp.Shared.Services
{
    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private const string AuthDataKey = "authData";
        private ClaimsPrincipal _currentUser;

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            if (_currentUser != null)
            {
                return new AuthenticationState(_currentUser);
            }

            string username = await SecureStorage.Default.GetAsync(AuthDataKey);

            if (!string.IsNullOrWhiteSpace(username))
            {
                var identity = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Name, username)
                }, "apiauth_type");

                _currentUser = new ClaimsPrincipal(identity);
            }
            else
            {
                _currentUser = new ClaimsPrincipal(new ClaimsIdentity());
            }

            return new AuthenticationState(_currentUser);
        }

        public async Task SignInAsync(string username)
        {
            await SecureStorage.Default.SetAsync(AuthDataKey, username);

            var identity = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name, username)
            }, "apiauth_type");

            _currentUser = new ClaimsPrincipal(identity);

            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public async Task SignOutAsync()
        {
            SecureStorage.Default.Remove(AuthDataKey);

            _currentUser = new ClaimsPrincipal(new ClaimsIdentity());

            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }
    }
}

Register Services in the MAUI Project

In YourApp.Mobile/MauiProgram.cs, register the authentication services:


// YourApp.Mobile/MauiProgram.cs
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using YourApp.Shared.Services;

namespace YourApp.Mobile
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();

            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                });

            builder.Services.AddMauiBlazorWebView();

    #if DEBUG
            builder.Services.AddBlazorWebViewDeveloperTools();
    #endif

            builder.Services.AddAuthorizationCore();

            // Register the custom authentication services
            builder.Services.AddScoped<CustomAuthenticationStateProvider>();
            builder.Services.AddScoped<AuthenticationStateProvider>(provider =>
                provider.GetRequiredService<CustomAuthenticationStateProvider>());

            builder.Services.AddScoped<ICustomAuthenticationService, CustomAuthenticationService>();

            // Register UserService
            builder.Services.AddScoped<UserService>();

            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; }
    }
}

Build Login and Logout Components

Create Login.razor

In YourApp.Shared/Pages, create Login.razor:


@page "/login"
@using YourApp.Shared.Services
@inject ICustomAuthenticationService AuthService
@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)
        {
            await AuthService.SignInAsync(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, create Logout.razor:


@page "/logout"
@using YourApp.Shared.Services
@inject ICustomAuthenticationService AuthService
@inject NavigationManager NavigationManager

<h3>Logging out...</h3>

@code {
    protected override async Task OnInitializedAsync()
    {
        await AuthService.SignOutAsync();
        NavigationManager.NavigateTo("/");
    }
}

Protect Pages with Authorization

Create a Protected Page

Create ProtectedPage.razor in YourApp.Shared/Pages:


@page "/protected"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject ICustomAuthenticationService AuthService

<h3>Protected Page</h3>

<p>Welcome, @userName!</p>

@code {
    private string userName;

    protected override async Task OnInitializedAsync()
    {
        var user = await AuthService.GetCurrentUserAsync();
        userName = user.Identity?.Name;
    }
}

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
                    {
                        <p>You are not authenticated. Please <a href="/login">log in</a>.</p>
                    }
                </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;
}

Test the Application

Run the Web App

  1. Start the Web Project (YourApp.Web).
  2. Navigate to a Protected Page: Visit /protected.
  3. Verify Redirection: You should see a message indicating you are not authenticated.
  4. Log In: Navigate to /login and use user1 / password1.
  5. Access Protected Content: You should now see the protected page.

Run the Mobile App

  1. Start the Mobile Project (YourApp.Mobile).
  2. Repeat the Steps: Perform the same steps as above to verify authentication on mobile.

Additional Considerations

Persisting Authentication State

In the MAUI app, authentication state is persisted using SecureStorage. In the Blazor Server app, authentication state is managed via cookies using ASP.NET Core’s authentication mechanisms.

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.
  • External Authentication Providers: Consider integrating with OAuth or OpenID Connect for robust authentication.

Conclusion

By following these steps, you’ve successfully implemented a basic authentication system in your .NET 9 MAUI Blazor Hybrid app that accounts for the limitations of server-side rendering in Blazor Server applications. This setup allows you to:

  • Understand Platform-Specific Authentication: Manage authentication appropriately in both web and mobile environments.
  • Share Code Effectively: 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.
]]>
https://blog.geosaffer.com/2024/11/20/187/feed/ 0 187