Converting an ASP.NET Core MVC App to Blazor SPA – Part 3 (Authentication + Wrap up)

Having built the front end in part 2, handling authentication in Blazor WebAssembly proved a little bit more work, but in the end it kinda worked. Microsoft’s documentation covers the basics of Blazor security for both WebAssembly and Server apps so its very well worth a read before reading on.

DISCLAIMER: There are more secure techniques available for SPA apps to authenticate against a backend API. For a more critical production system, I’d opt (and strongly advise) to invest in those techniques. Bottomline: don’t store auth tokens in browser because browser storage is visible to all the 3rd-party scripts. Anyone of them getting compromised could result in the token being sniffed. I did it for this app because its an easy way to get auth working not necessarily the right (or more secure) way.

The central idea around Blazor auth is for the UI system to be able to query the authentication state of the user and render different views. Blazor uses AuthenticationStateProvider to query this auth state via the AuthorizeView component. This component provides the Authorized and NotAuthorized nested components that can be used to conditionally render the UI:

<AuthorizeView>
<Authorized>
@{
navMgr.NavigateTo("/");
}
</Authorized>
<NotAuthorized>
<MatCard class="mat-card">
<MatCardContent>
<div class="mat-card-content">
<MatHeadline6 class="mat-card-clean-margin">
E-mail
</MatHeadline6>
<MatTextField Class="mat-card-field" @bind-Value="Username"></MatTextField>
<MatHeadline6 class="mat-card-clean-margin">
Password
</MatHeadline6>
<MatTextField Class="mat-card-field" Type="password" @bind-Value="Password"></MatTextField>
</div>
</MatCardContent>
<MatCardActions Class="mat-card-action">
<MatButton Raised="true"
OnClick="@(async args => await AuthenticateUser())">Login</MatButton>
</MatCardActions>
</MatCard>
</NotAuthorized>
</AuthorizeView>
view raw Login.razor hosted with ❤ by GitHub

If the user is not authorised, show them the login fields otherwise, take them to the home page.

Essentially the job of the AuthenticationStateProvider is to let the application know if the current user is authenticated or not. The only requirement for this implementation is to return a Task<AuthenticationState> when the framework calls into GetAuthenticationStateAsync() which it does at certain points in time.

public class MyAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly IAuthenticationService authService;
private readonly IClientTokenStorage clientTokenStorage;
private AuthenticationState currentAuthenticationState;
public MyAuthenticationStateProvider(
IAuthenticationService authService,
IClientTokenStorage clientTokenStorage)
{
this.authService = authService;
this.clientTokenStorage = clientTokenStorage;
this.authService.UserLoggedIn = async () =>
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
};
this.authService.UserLoggedOut = async () =>
{
await this.clientTokenStorage.RemoveToken(AuthConstants.AUTH_TOKEN_KEY);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
};
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var authToken = await clientTokenStorage.GetToken(
AuthConstants.AUTH_TOKEN_KEY);
if (string.IsNullOrWhiteSpace(authToken) ||
HasTokenExpired(authToken))
{
await clientTokenStorage.RemoveToken(AuthConstants.AUTH_TOKEN_KEY);
currentAuthenticationState = new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity()));
}
else
{
currentAuthenticationState = new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity(
JwtClaimsParser.GetClaims(authToken),
"password")));
}
return currentAuthenticationState;
}
private bool HasTokenExpired(string authToken)
{
var claims = JwtClaimsParser.GetClaims(authToken);
var expiryClaim = claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp);
var expiresOn = long.Parse(expiryClaim.Value);
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expiresOn;
}
}

How do I make the auth provider aware of when the user logs in or logs out so it can notify the views and components of the authentication state? To solve this, I came up with the following monstrosity:

Client Side Auth Architecture for my Blazor App

Before I walk you through this maze, recall from part 1 that I had created an API client package that client apps can use to talk to the backend api. This client package amongst other nice things, also automatically stamps the outgoing requests with the authorisation header. For it to be able to do this, it asks consumers to provide an implementation for IAuthTokenRepository i.e. a custom interface defining the blueprint for a place to store tokens into and pull them back out of.

Ok, here we go! First the login flow:

Step 1: User types in their username and password into the login page and hits Login

Steps 2,3,4 & 5: This act invokes the Authenticate method on AuthenticationService (custom class) which delegates the call to the API client package which in turn verifies the credentials against the API. The API, if the credentials are valid, returns a JWT token containing user claims. Note: JWTs should not contain PII or any other sensitive data like secrets or passwords.

Steps 6,7 & 8: The API client package puts the auth token into the client provided token repository. In this case, this underlying storage is browser session storage

Steps 9, 10 & 11: AuthenticationService raises a custom UserLoggedIn event (in reality its a callback) that is subscribed to by the AuthenticationStateProvider. At this stage, the provider gets the token from the storage, validates it for expiry, creates a new Principal from the claims data and notifies the views of this change in authentication state. This notification is built into the Blazor framework, all I had to do was make sure the framework knows about authentication happenings.

From here on out, anytime the user takes an action for e.g. see past expenses etc, the token will be pulled out of the storage and stamped into the outgoing request by the API client package automatically. This is a bit safer from an XSRF perspective, since auth tokens won’t be sent by the browser automatically unlike cookies (unless they are packaged in cookies ofcourse).

Logout flow:

Step 13. User hits logout

Step 14, 15 & 16. The action is delegated to the AuthenticationService which promptly removes the token from the client store and raises the custom UserLoggedOut event.

Steps 17 & 18. The AuthenticationStateProvider clears the Principal and notifies the views of the state change. The user is then navigated back to the login page.

If the authentication fails, the API client throws an AuthenticationFailed exception which I handle in views individually and force a logout. I admit the code is a bit repetitive and I would like to move it into a more central spot where it can apply at a more horizontal level to all the views. For now though, this will do I don’t have that many views anyway.

private async Task GetCurrentBudget()
{
try
{
CurrentBudget = (await traxpenseApi.GetCurrentBudget(
ClientSession.CurrentPeriod.PeriodId)).ToViewModel();
}
catch (AuthenticationFailed)
{
await authService.Logout();
}
}

Finally, I need to wire these up in the DI:

public static IServiceCollection AddTraxpenseAuthentication(
this IServiceCollection services)
{
// register the framework service for auth
services.AddAuthorizationCore();
// register custom dependencies
services.AddSingleton<IClientTokenStorage, SessionTokenStorage>();
services.AddScoped<AuthenticationStateProvider>(
serviceProvider =>
new MyAuthenticationStateProvider(
serviceProvider.GetService<IAuthenticationService>(),
serviceProvider.GetService<IClientTokenStorage>()));
services.AddScoped<IAuthenticationService, AuthenticationService>();
return services;
}
view raw DIWireup.cs hosted with ❤ by GitHub

Any view that needs to be secured only has to add the Authorize attribute at the top of the page and its good to go:

@inject ITraxpenseApiClient traxpenseApi
@attribute [Authorize]
<!--view code-->
<!--C# code-->
@code {
}
view raw AuthedView.razor hosted with ❤ by GitHub

The App.razor will also need to be modified to be auth aware and use the AuthorizeRouteView component to navigate the users to login page if they are not authenticated:

<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@{
NavMgr.NavigateTo("/login");
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(NotFoundLayout)">
<p>Oops! That page wasn't found! You may want to return <a href="/">Home</a></p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>
view raw App.razor hosted with ❤ by GitHub

Finally, wherever I want to conditionally render the UI based on the authentication state, I can simply use this template:

@inherits LayoutComponentBase
<AuthorizeView>
<Authorized>
<div class="sidebar">
<!-- render auth sensitive views if user authenticated -->
</div>
</Authorized>
<NotAuthorized>
<div>
"Sorry!"
</div>
</NotAuthorized>
</AuthorizeView>
@code{
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
//...other code
}

As a matter of convention, every component that needs to use the conditional rendering, needs to also expose a CascadingParameter of type Task<AuthenticationState> in the code block. This is how the conditional rendering component will know if the user is authenticated or not. I can also use this same parameter property to take some corresponding action in the C# code block.

Like I mentioned before, storing auth tokens on client side is a weak security strategy because all scripts can access browser storage so the auth tokens can easily be sniffed out and abused (at least as long as they don’t expire). Going forward I will be looking at flows like OAuth2 PKCE that’s designed to be used with client apps (SPA and mobile apps alike) to acquire/use auth tokens without actually storing them anywhere on the client side thus minimising risks like XSS and token leakage.

Conclusion:

With these blog posts and the whole refactoring effort I have only scratched the surface of Blazor and taken a few well considered architecture decisions, a few design liberties and made a couple of sub-optimal choices. The system is by no means done or perfect so I am sure it might change in significant ways again, but I did and do enjoy the continuous process of exploration, experimentation and discovery whilst building software, especially when learning something new.

Its fine to fumble and stumble to get something working but once that super-excitement phase has calmed down its always good to come back and peel back the layers of complexity and to see just what kind of magic the framework is doing for you. Its fine if you don’t understand everything from the get go, its a continuous process but you should try and not be in a position of complete unawareness because that’s when the framework can bite you!

Finally, its fun to see how such a simple system can evolve so much over the years. Things that you think you will or might never do, come back to you and that’s where taking some carefully thought out architectural decisions can make a huge difference. I can’t even imagine plugging a different UI on a 10 year old version of this system due to everything being tightly coupled together but constant refactoring to reduce tight coupling resulted in a design that can support multiple consumers today.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.