Search code examples
c#authenticationasp.net-web-apiasp.net-identitymaui

.NET MAUI - Can I use Authentication WebAPI from existing Blazor project?


We already have a running Blazor WebAssembly App (ASP.NET Core hosted) and implemented Authentication/Authorization according to this tutorial. We added Roles to the project in order to restrict users to certain content. The backend is using Microsoft.AspNetCore.Identity as NuGet dependency.
We want to implement a .NET MAUI App for this project, which should use the same database for the users, so no one has to register again.
We still want to use the same backend for our Blazor application, therefore the solution must be compatible with both Blazor and .NET MAUI.
Is there a way for .NET MAUI to use the same Users/Roles which were created with Microsoft.AspNetCore.Identity? If possible, how do we render UI elements based on the role of the user?


Solution

  • @philipp8230, sorry it has taken so long to get back to you. I've actually been working on a project trying to get NTLM/Negotiate working from .Net Maui and decided to punt and redesign our server side with .Net 8 using Identity with individual accounts. There are some new neat .8 features allowing Identity APIs.

    I converted my existing .7 server side to .8 Blazor Web App/Web Assembly including Identity with SQL individual accounts and customization to ApplicationUser:IdentityUser. I also have some custom component pages for managing user and roles.

    Next, I exposed the existing Identity API endpoints by changing this line in Program.cs:

    builder.Services.AddIdentityCore<ApplicationUser>(options =>
        options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddSignInManager();
    

    to

        builder.Services.AddIdentityApiEndpoints<ApplicationUser> (options =>
            options.SignIn.RequireConfirmedAccount = true)
            .AddRoles<IdentityRole>()    
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddSignInManager();
     //For Swagger
     builder.Services.AddEndpointsApiExplorer();
    ...
    app.MapIdentityApi<ApplicationUser>();
    

    On the Maui Client, I opted to consume the Api of my server side using Tokens offered by the Identity Api Endpoints. For pages I want to share in both server side and Maui, I moved to Razor Class Libraries with pages and views. These pages process data through the server side Api with the Authorize attribute. I created a shared service for an HttpClient that manages the login(initial token), refresh token, etc. Code sample of obtaining a token using Identity Individual Credentials:

    private async Task Login()
    {
        if (BearerToken == null)
        {
            try
            {
                var loginResponse = await client.PostAsJsonAsync("/login", new { email = UserName, password = Password });
                if (loginResponse.IsSuccessStatusCode) {
                    BearerToken = await loginResponse.Content.ReadFromJsonAsync<ClientToken>();
                    BearerToken.expiresOn = DateTime.Now.AddSeconds(BearerToken.expiresIn - 1800);
                    client.DefaultRequestHeaders.Authorization = new("Bearer", BearerToken.accessToken);
                }
                else
                {
                    BearerToken = null;
                }
    
            }
            catch(Exception ex)
            {
                BearerToken = null;
            }
        }
        else
        {
            try
            {
                if (BearerToken.expiresOn > DateTime.Now)
                {
                    var loginResponse = await client.PostAsJsonAsync("/refresh", new { refreshToken = BearerToken.refreshToken });
                    if (loginResponse.IsSuccessStatusCode)
                    {
                        BearerToken = await loginResponse.Content.ReadFromJsonAsync<ClientToken>();
                        BearerToken.expiresOn = DateTime.Now.AddSeconds(BearerToken.expiresIn - 1800);
                        client.DefaultRequestHeaders.Authorization = new("Bearer", BearerToken.accessToken);
                    }
                    else { BearerToken = null; }
    
                }
            }
            catch(Exception ex)
            {
                BearerToken = null;
            }
    
        }
        return;
    }   
    

    I placed a call to login() before my base Get, Put, Post, etc. for my application endpoints. If an existing current Token exists, login() will return, else either refresh or login again via the Identity Api Endpoints.

    On the Maui client, I added the HttpClient Service as singleton to maintain the Token:

        builder.Services.AddSingleton<HttpService>();
    

    I also added a Login.razor page and in HttpService a public login(string username, string secret) to get an initial Token from the user.

    So far so good, I've got the same user list, roles, Authorization, etc. As a side note, I also added the new .Net 8 Identity Razor Component Pages to replace the older razor pages with .cshtml code behind on the server side with:

    // Add additional endpoints required by the Identity /Account Razor components.
    app.MapAdditionalIdentityEndpoints();