Skip to content
Menu
GeoSaffer.com
  • Shop
  • Support
GeoSaffer.com

Implementing Authentication in a .NET 9 MAUI Blazor Hybrid App

Posted on November 18, 2024November 18, 2024

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

  1. Project Structure Overview
  2. Prerequisites
  3. Step-by-Step Implementation
    • Create a New Solution
    • Implement the Authentication State Provider
    • Implement a Simple User Store
    • Build Login and Logout Components
    • Protect Pages with Authorization
    • Update the Navigation Menu
    • Adjust Routing in Routes.razor
    • Test the Application
  4. Additional Considerations
    • Persisting Authentication State
    • Enhancing Security
  5. 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)

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

  1. Start the Web Project (YourApp.Web).
  2. Navigate to a Protected Page: Visit /protected.
  3. Verify Redirection: You should be redirected to the login page.
  4. Log In: 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

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!

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Categories

  • 3D Printing
  • Apps
  • Drone Footage
  • Electronics
  • Uncategorized
©2024 GeoSaffer.com | WordPress Theme by Superbthemes.com