Can Of Code

Blazor WebAssembly .NET5 – First look and some gotchas

What is Blazor WebAssembly?

Blazor can be described most simply as a frontend framework like Angular but instead of JavaScript or TypeScript we get to use C# (that is where WebAssembly comes into play). Because we are using C# for the frontend we can share some code between the front and back and lets face it, use a language that we are comfortable with.

There are some drawbacks to such as the initially load time is likely to be slower because the client has to download the assembly before they can get started.

There is a Server based model for Blazor which does not rely on WebAssembly and instead does most of the processing server side. you can get a much better explanation of it here.

Identity gets complex

Like any client sides technology, handling authentication always becomes a little more complex than using a cookie based approach. This has been something that I have always felt held me back from using SPAs in the past. You have to  investigate methods such as OpenId to exchange tokens. This is not anything new for say mobile app development but it is more complex to get your head around then the cookie based. The tooling around OpenId is dependant on third party packages such as IdentityServer. Would be nice if this part of the framework but there is plenty of support and documentation.

Helpful Blazor Project Template

The Visual Studio Blazor template thankfully comes with IdentityServer already setup which saves you a lot of time learning. Its also runnable from the start, running your backend server in debug while also running the client. One thing I wish the template had setup was an example of authenticated POST of data and linking that data to the logged in user.

Trouble getting the Logging in User in the Controller

One early issue I had was getting the current logged in user object using the User Manager. Running say:

var user = await _userManager.GetUserAsync(User);

would always return null even though User had a valid identity. Turns out this is related to GetUserAsync looking at the wrong claim to get the users Id . You can therefore use something like below to get the users Id then look up the user using the DatabaseContext

var id = User.FindFirstValue(ClaimTypes.NameIdentifier);
return _context.Users.FirstOrDefault(x => x.Id == id);

What is the upfront download / loadtime?

As a debug build with the Blazor template we are looking at 9.6Mb download on first load but they do fire out a console log message promising it will be smaller when you publish. So after a publish we are still looking at 3.7mb weight which admittedly not that bad considering its including some bootstrap styling and all the logic and should only be at the initial visit. As a really unscientific measure the initial load times even with the resources cached takes 10-15 seconds which would be impractical for a simple website but I suspect it would be understandable for a complex application that users would expect some load time.

The Client can be hosted anywhere?

Yes but you do need some sort of server to help with routing for example if you go to test.local/things then you need a server to route /things to the index.html. I was able to get the Client running on a local Apache LAMP setup using the configuration discussed here 

I did a separate post: blazor webassembly on a different host to the server which explains the process I went through to setup the Client on a different host to the server.

Posted in Blazor |

Blazor WebAssembly on a different host to the Server

If you open up the Blazor WebAssembly project template it is configured to run both the client and the server sharing the same host localhost:5001 for example. If you want to host your client separately from the server you have to change a few settings on both the client and server which wasn’t clear out the gate, I have created a walkthrough on how I achieved this and hopefully its some use to you too.

Who is this for?

  • Want to have the Blazor WebAssembly client hosted on a different URL say  client.myexample.com and your API on api.myexample.com
  • Those new to Blazor (like myself)

CORS

Because we are accessing the Server from a different host than the API we have to configure CORS to allow access. What is less than ideal is that you do have to set the CORS for IdentityServer and for ASP Core separately.

In the Server project I added the following to the ConfigureServices method in Startup.cs

var _loggerFactory = new LoggerFactory(); 
var cors = new DefaultCorsPolicyService(_loggerFactory.CreateLogger<DefaultCorsPolicyService>()) 
{ 
AllowedOrigins = { "https://client.myexample.com"} 
}; 
services.AddSingleton<ICorsPolicyService>(cors);

This adds our client host to the Allowed origins that is used by IdentityServer. We now need to also allow our client to the ASP Core Allowed Origins. There are multiple ways to do this but I had success using the following in the Configure Method in the servers Startup.cs.

app.UseCors(policy => { 
    policy.AllowAnyHeader(); 
    policy.AllowAnyMethod(); // This was added to stop blocking DELETE requests 
    policy.WithOrigins("https://client.myexample.com"); 
});

This should at least allow us to send requests from our client hosted at client.myexample.com to our server at api.myexample.com

Setting up the OIDC Client

OpenID Connect (OIDC) uses clients to keep track of things like redirect urls allowing you to have a different client for different frontends, say Mobile Apps or SPA. in the Blazor example, these are set in the appsettings.json and the default setup looks like this:

"IdentityServer": { 
"Clients": {
"WhatsForDinDins.Client": { "Profile": "IdentityServerSPA" } }
}

While this keep things nice and tidy for someone new, it does hide the configuration we need to set if we are not based on the same host. After a lot of trial and error I found the following configuration allowed us to authenticate with a client on a different host:

"IdentityServer": {
"Clients": {
"ProjectName.Client": {
"Profile": "SPA",
"RedirectUri": "https://client.myexample.com/authentication/login-callback/",
"LogoutUri": "https://client.myexample.com/authentication/logout-callback/",
"AllowedScopes": [ "ProjectName.ServerAPI", "openid", "profile" ]}
}
},

Here we are setting the clientId as “ProjectName.Client” and telling IdentityServer that it should callback to our client domain after it has authenticated. We also set the allowed scopes with “ProjectName.ServerAPI” being the API. Although I’m not sure this is required. The Profile value I’m still not sure but I found that setting SPA was what worked where as IdentityServerSPA did not. 

At this point our Server should be set to allow authentication and controller requests from our externally hosted client at client.myexample.com

Setting up the Blazor client

First we need to update our HttpClient to use our server host base address instead of the default which assumes the server is located on the same host as the client.

builder.Services.AddScoped<CustomAuthorizationMessageHandler>();

      
builder.Services.AddHttpClient("ProjectName.ServerAPI", client => client.BaseAddress = new Uri("https://api.myexample.com"))
                .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

You may notice that we have a CustomAuthorizationMessageHandler. We have created a class that is used to ensure that the Authorization knows to look for our server instead of assuming its located on the same domain.

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
        public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
            NavigationManager navigationManager)
            : base(provider, navigationManager)
        {
            ConfigureHandler( 
                authorizedUrls: new[] { "https://api.myexample.com" },
                scopes: new[] { "ProjectName.ServerAPI", "openid", "profile" });
        }
    }

Lastly in our Program.cs of the client we need to replace the default API Authentication and replace it with our own configuration values using AddOidcAuthentication:

builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.MetadataUrl = "https://api.myexample.com/.well-known/openid-configuration";
    options.ProviderOptions.ClientId = "ProjectName.Client";
    options.ProviderOptions.Authority = "https://api.myexample.com/";
    options.ProviderOptions.ResponseType = "code";
    options.ProviderOptions.RedirectUri = "https://client.myexample.com/authentication/login-callback/";
    options.ProviderOptions.PostLogoutRedirectUri = "https://client.myexample.com/authentication/logout-callback/";
    options.UserOptions.ScopeClaim = "ProjectName.ServerAPI openid profile";
 });

And with that we should be set up to authenticate and call authorized controller actions from a client hosted on a separate host from its server.

It might not be the best way..

I came to this solution as a beginner to Blazor and with a rough understanding of OIDC so there maybe a better way to configure this without having to change so much configuration. Perhaps you only have to set the CORS in one location? Happy to hear if you have a different solution and of course I will update this post if I find out more. 

Posted in Blazor | Tagged , |