• Jul 29, 2024

Secure Your Blazor Server/Client App and API with Auth0: A Step-by-Step Guide

    In this post, we're tackling one of the most critical aspects of building modern web applications: authentication. Specifically, we're going to explore how to add robust authentication to your Blazor app and integrate it seamlessly with your ASP.NET Core API using Auth0.

    By the end of this guide, you'll have a solid understanding of how to add secure communication between Blazor apps (both client-side and server-side components) and your API, ensuring a seamless user experience while maintaining the highest level of security.

    Adding Authentication to Your Blazor App and ASP.NET Core API

    If you're new to Blazor or haven't integrated authentication before, I recommend starting with this Auth0 article. It provides a comprehensive guide on how to add authentication to your Blazor app. Follow the steps outlined in this article to ensure that your Blazor app is secure and ready for further integration.

    Once you've added authentication to your Blazor app, it's time to focus on your ASP.NET Core API. This quick-start provides a step-by-step guide on how to integrate Auth0 into your API. It covers the necessary steps to secure your API and ensure that only authenticated requests are processed.

    Integrating Authentication Across Components

    While both components are now secure, there's still a gap in our authentication setup. We need to integrate the authentication between the Blazor app and ASP.NET Core API. To do this, we'll request an access token from Auth0 and include it in our backend requests. This means adding a .WithAccessToken() call to the the Blazor Server configuration.

    builder.Services.AddAuth0WebAppAuthentication(options =>
        {
            options.Domain = auth0Options.Domain;
            options.ClientId = auth0Options.ClientId;
            options.ClientSecret = auth0Options.ClientSecret;
    
            options.Scope = "openid profile";
        })
        .WithAccessToken(options =>
        {
            options.Audience = auth0Options.ApiAudience;
            options.UseRefreshTokens = true;
        });

    The auth0Options variable is just an IOptions implementation to load the configuration.

    The access token will be accessed via the HTTP context and that's how you'll include it in the request. I prefer to do this in a HTTP client handler, so that the token is added just before the request is sent.

    public class ServerTokenHandler(IHttpContextAccessor httpContextAccessor)
        : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var httpContext = httpContextAccessor.HttpContext;
            if (httpContext is null)
            {
                throw new UnreachableException(
                    $"HttpContext not set in {nameof(ServerTokenHandler)}.{nameof(SendAsync)}().");
            }
    
            var accessToken = await httpContext.GetTokenAsync("Auth0", "access_token");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    
            return await base.SendAsync(request, cancellationToken);
        }
    }

    Your backend request will now include the access token in every request you make. You can extend this code to make only add the header if there is a token, and if you're sending the request to your backend URL, but I'll leave this up to you to implement.

    Implementing a BFF Pattern on the Client Side

    While the previous code works for Blazor Server, as soon as you implement an InteractiveWebAssembly or InteractiveAuto render mode, your request fail. The reason is that HTTP context is not available on the client side, so you'll need to have a different strategy.

    To get the token on the client side, we need to implement a Backend-For-Frontend (BFF) pattern. This involves creating a server-side endpoint that takes the access token from Auth0 and returns it to the Blazor client side. The simplest way is to create a minimal API endpoint that outputs the token.

    app.MapGet(NavigationUrls.Token,
        async (HttpContext httpContext) =>
        {
            var accessToken = await httpContext.GetTokenAsync("Auth0", "access_token");
            return !string.IsNullOrEmpty(accessToken)
                ? Results.Text(accessToken, "text/plain", Encoding.UTF8)
                : Results.NotFound();
        });

    Note that I have a constant for all my well-known urls, so that it's easier to refactor. Also, make sure this endpoint is only allowed to authenticated users!

    In order to get the token and include it on the client-side, you'll need two different components: an HTTP client that gets the token from that endpoint and a handler to include it on the requests.

    public class AuthenticationHttpClient(HttpClient httpClient)
    {
        public async Task<string> GetAccessToken()
        {
            return await httpClient.GetStringAsync(NavigationUrls.Token);
        }
    }

    Using the Token in the Backend Request

    Now that we have the token on the client side, we can use it to authenticate our requests to the ASP.NET Core API. We'll include the token in the headers of our request to ensure that only authenticated requests are processed, similar to what we did in the server-side.

    public class ClientTokenHandler(AuthenticationHttpClient authenticationHttpClient) : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var accessToken = await authenticationHttpClient.GetAccessToken();
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            return await base.SendAsync(request, cancellationToken);
        }
    }

    Conclusion

    With authentication fully integrated into your stack, you've taken a significant step towards securing your application and ensuring a seamless user experience. Also, by implementing a BFF pattern on the client side, you've enabled secure communication between your frontend and backend components.

    Now that you have a solid foundation for authentication in place, it's time to think about how you can further optimize your implementation. For example, you might consider caching the token (whether using session storage or a scoped service) to improve performance and reduce the number of requests made to your BFF endpoint.

    The possibilities are endless, but one thing is certain: with this foundation in place, you're well on your way to building a secure and scalable application that meets the needs of your users.

    0 comments

    Sign upor login to leave a comment