authenticationarchitectureblazorwebapi.net-8.0

.NET8 Auth - what should be the architecture for a real world usage?


I heard that for .NET8 Microsoft gifted us with a totally "fixed" authentication and authorisation setup.

Now, when I create a Blazor application from the templates, it scaffolds out a whole bunch of user admin pages including login etc., and there is a database migration you can run to create the associated backend support for it. And it works great, right out of the box I can add users, login, and protect pages with the [Authenticate] flag.

The first problem I have is I don't want to use Entity Framework, and I don't want to use MS SQL Server. But that's trouble for another time.

My wider problem is this out-of-the-box experience provided in the Blazor project template does not attempt to address one of the typical architectures where you have a Blazor app for the front-end, a database for the backend, and an API that sits in between to receive and reply to requests.

A typical use-case

In this scheme, which is the one that I want to use, the identity should not be handled directly by the Blazor app. Instead, it is really the WebAPI that should do it since it is the one who has access to the database backend.

But if I use the templates to create a .NET8 API project, Microsoft sticks with the previous AzureAD authentication method (from previous .NET versions) and does not use any of the new Identity stuff that you get provided with the Blazor application.

OK, so perhaps, I can see what they are doing in the Blazor App and implement all that on the API side instead. But now I am wondering if I am going against the grain here, and doing something which Microsoft does not intend. Essentially, I am worried I am lost at Sea and doing something wrong.

There is the further complication that even if I implement all that identity in the API, I still need to return some kind of identity object to the Blazor app, since it also needs to know if the user login was OK and what roles they have. I can imagine some ways that I could call an API endpoint to login from the Blazor app and return a suitable identity object to the Blazor app. But what about the potential for middle-man attacks here where someone would send their own identity object. Is it protected because of the https:// connection between the Blazor App and the WebAPI?

My question in summary: Is my setup of WebAPI for the identity, return the identity object to Blazor, a typical solution or am I down the wrong path with this?


Solution

  • I have always asked myself this very same question: why Microsoft templates and even documentation offer so little for those who want a more production-ready system?

    The approach I have settled on for a while now is to:

    1. Create a Blazor Web Assembly project for the UI using Azure Entra External ID (previously Azure AD B2C) for authentication. I tend to publish this via Azure CDN.
    2. Create a Functions App for the API and configure Easy Auth. The most challenging aspect here is to configure Easy Auth to be used with Azure Entra External ID (it's trivial to configure it for Azure Entra, but not so much for Azure Entra External ID).

    Providing a full explanation of how to do the above is beyond the scope of a SO answer... if you can't find it I can try to put of a blog post together.

    I hope this is at least useful to push you in the right direction.

    UPDATE: this is my attempt to provide more guidance on how to proceed.

    1. Create an Azure Entra External ID tenant: https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
    2. Create a sign-in user flow. Feel free to include a sign-up flow too if your requirements allow for it: https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-user-flow
    3. Optional: it's also recommended to add a password reset user flow.
    4. Register an application in the B2C tenant: https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-register-applications?tabs=app-reg-ga.
    5. Create an App Service for your API (could be an Azure Function, which is hosted on App Service).
    6. Enable the built-in authentication, a.k.a. Easy Auth, using the "Custom Provider" option. Use a URL like this, replacing xxxx with your B2C Tenant name and B2C_1_Signin with the name of your own sign-in flow: https://xxxx.b2clogin.com/tfp/xxxx.onmicrosoft.com/B2C_1_Signin/v2.0/.well-known/openid-configuration
    7. Use the Client ID of the App Registration you created in step 4.
    8. You are now free to deploy your Blazor Web Assembly to an Azure CDN, which I strongly recommend even though it comes with its own challenges about caching and integrity verification when a new version is published.
    9. I'm not sure about the integration of Easy Auth with ASP.NET Core, but in Azure Functions Isolated (recommended) you'll get the OAuth claims in the HttpRequestData received as a parameter of the HttpTrigger function (req.Identities and req.Headers): https://docs.microsoft.com/en-us/azure/app-service/configure-authentication-user-identities
    10. Don't forget to add your Blazor Web Assembly app's URL to the Allowed Origins of the App Service under the CORS section.

    This is the function I use to extract all the important information into a record we call BackendSecurityContext:

    public class EasyAuthHeadersAzureFunctionsSecurityContextFactory : IAzureFunctionsSecurityContextFactory
    {
        public const string HeaderName_PrincipalID = "x-ms-client-principal-id";
        public const string HeaderName_PrincipalName = "x-ms-client-principal-name";
    
        public IBackendSecurityContext CreateSecurityContext(IEnumerable<ClaimsIdentity> claimsIdentities, ImmutableDictionaryOfSet<string, string> httpRequestHeaders)
        {
            // Supporting documentation can be found here: https://docs.microsoft.com/en-us/azure/app-service/configure-authentication-user-identities#access-user-claims-in-app-code
            var idStr = httpRequestHeaders[HeaderName_PrincipalID].SingleOrDefault();
            var name = httpRequestHeaders[HeaderName_PrincipalName].SingleOrDefault();
    
            var result = new BackendSecurityContext(
                idStr is null ? null : Guid.Parse(idStr), // PrincipalID
                name, // PrincipalName
                !string.IsNullOrEmpty(idStr), // IsAuthenticated
                [], // PrincipalClaims
                ImmutableListWithSequenceEquality<string>.Empty // Roles
            );
    
            return result;
        }
    }
    

    Note: in my opinion, this should all be covered by a single Microsoft documentation page as an end-to-end guide to serious/production-ready development and deployment of a web app, but I have never found such a page. If someone does, they should just replace my answer with a link! :-)