Session Authentication with ASP.NET Core 3.1 Blazor Server Side

There are a lot tutorials online on “howto implement authentication with .NET Core Blazor Server Side”; comprising Windows Authentication or a cookie based approach, which isn’t even supported yet.

Microsoft somewhat confirmed that cookie support may be added within the future, but for now, we need to stick with a workaround. In case you don’t wanna wait, nor stick with the workaround, I present you: A simple, straight forward, session-based authentication implementation.

Is it quick? Yes
Is it dirty? Maybe
Is it secure? *cough*, somewhat

Sounds great?! Then lets go; Spoiler: Reading isn’t much fun. Thus, go ahead; skip the tutorial and get right into the implementation details.

Install dependencies

Reinventing the wheel sucks, so, let’s add some NuGets

Install-Package AuthenticatedEncryption -Version 1.0.1
Install-Package Microsoft.AspNetCore.ProtectedBrowserStorage -Version 0.1.0-alpha.19521.1
  • AuthenticatedEncryption: We don’t want to slap our session object into the user’s face, without even pretending to know what we are doing.
  • ProtectedBrowserStorage: Session storage Read/Write operations.

Add Services

Now that all essential dependencies have been added, let’s edit the Startup.cs file. Add following services to the IServiceCollection.

services.AddRazorPages();
services.AddServerSideBlazor();
services.AddAuthorization();
services.AddAuthentication();
services.AddProtectedBrowserStorage();
services.AddScoped<AuthenticationStateProvider, UserService>();
services.AddHttpContextAccessor();
services.AddHttpClient();

Well, UserService won’t resolve yet, since this baby is our actual authentication/authorization implementation.

Authentication Implementation

AuthenticationStateProvider is responsible for the whole Authentication and Authorization flow, implemented by our UserService.

    public class UserService : AuthenticationStateProvider
    {
        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
	        return base.GetAuthenticationStateAsync();
        }
    }

GetAuthenticationStateAsync will be called once after the page has been loaded, and its return value contains the user’s permission set. For any further permission fetch (e.g.: re-evaluation after login) NotifyAuthenticationStateChanged needs to be called.

In our case, LoginAsync and LogoutAsync, both do manipulate the session object, followed by a NotifyAuthenticationStateChanged, forcing the AuthenticationStateProvider to re-evaluate.

    public class UserService : AuthenticationStateProvider
    {
        private const string USER_SESSION_OBJECT_KEY = "user_session_obj";

        private ProtectedSessionStorage protectedSessionStore;
        private IHttpContextAccessor httpContextAccessor;

        public UserService(ProtectedSessionStorage protectedSessionStore, IHttpContextAccessor httpContextAccessor) =>
            (this.protectedSessionStore, this.httpContextAccessor) = (protectedSessionStore, httpContextAccessor);

        public string IpAddress => httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString() ?? string.Empty;

        private User User { get; set; }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            // read a possible user session object from the storage.
            User userSession = await GetUserSession();

            if (userSession != null)
                return await GenerateAuthenticationState(userSession);
            return await GenerateEmptyAuthenticationState();
        }

        public async Task LoginAsync(User user)
        {
            // store the session information in the client's storage.
            await SetUserSession(user);

            // notify the authentication state provider.
            NotifyAuthenticationStateChanged(GenerateAuthenticationState(user));
        }

        public async Task LogoutAsync()
        {
            // delete the user's session object.
            await SetUserSession(null);

            // notify the authentication state provider.
            NotifyAuthenticationStateChanged(GenerateEmptyAuthenticationState());
        }

        public async Task<User> GetUserSession()
        {
            if (User != null)
                return User;

            string localUserJson = await protectedSessionStore.GetAsync<string>(USER_SESSION_OBJECT_KEY);

            // no active user session found!
            if (string.IsNullOrEmpty(localUserJson))
                return null;

            try
            {
                return RefreshUserSession(JsonConvert.DeserializeObject<User>(localUserJson));
            }
            catch
            {
                // user could have modified to local value, leading to an
                // invalid decrypted object. Hence, the user just did destory
                // his own session object. We need to clear it up.
                await LogoutAsync();
                return null;
            }
        }

        private async Task SetUserSession(User user)
        {
            // buffer the current session into the user object,
            // in order to avoid fetching the user object from JS.
            RefreshUserSession(user);

            await protectedSessionStore.SetAsync(USER_SESSION_OBJECT_KEY, JsonConvert.SerializeObject(user));
        }

        private User RefreshUserSession(User user) => User = user;

        private Task<AuthenticationState> GenerateAuthenticationState(User user)
        {
            ClaimsIdentity claimsIdentity = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name, user.Id.ToString()),
                new Claim(ClaimTypes.Role, user.Role.ToString())
            }, "apiauth_type");

            ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            return Task.FromResult(new AuthenticationState(claimsPrincipal));
        }

        private Task<AuthenticationState> GenerateEmptyAuthenticationState() => Task.FromResult(new AuthenticationState(new ClaimsPrincipal()));
    }

Even thought the User class isn’t essential for the proof of concept, feel free to copy and paste.

    public enum UserRole : int
    {
        Administrator = 2,
        Moderator = 1,
        User = 0
    }

    public class User
    {
        public long Id { get; set; }

        public string Alias { get; set; }

        public string EmailAddress { get; set; }

        public UserRole Role { get; set; } = UserRole.User;

        public string Password { get; set; }
    }

IServiceProvider Extension

Blazor isn’t able to inject service implementations, like our UserService. We can access AuthenticationStateProvider, but not its actual implementation. Thus, this extension will come in quite handy later on.

    public static class ServiceProviderExtensions
    {
        public static T Get<T>(this IServiceProvider serviceProvider) => (T)serviceProvider.GetService(typeof(T).BaseType);
    }

_Imports.razor

We are almost done – I promise! To access the UserService any time, add the IServiceProvider and the extension namespace to the _Imports.razor.

@using YOUR_ ServiceProviderExtensions _NAMESPACE

@inject IServiceProvider ServiceProvider
@inject NavigationManager NavigationManager

_Host.cshtml

Set the render-mode to Server, in order to avoid additional complexity. Furthermore, include the protectedBrowserStorage.js, to facilitate reading and writing the local Session storage. (Our ProtectedBrowserStorage Nuget package depends on that javascript file)

...
<component type="typeof(App)" render-mode="Server" />
...

<script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>

App.razor

To propagate the AuthenticationState through all of your components, wrap the <Router /> into a <CascadingAuthenticationState />

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
       ...
    </Router>
</CascadingAuthenticationState>

Login/Logout – The reason why you are here

After validating the user’s input, calling await ServiceProvider.Get<UserService>().LoginAsync(User); will set the session object and refresh the authorization configuration.

@page "/"


    <div class="container">

        <div class="row justify-content-md-center mb-5">
            <div class="col-md-auto">
                
                <button @onclick="LoginUserRequest">Login</button>
                <button @onclick="LogoutUserRequest">Logout</button>

            </div>
        </div>

        <AuthorizeView>
            <Authorized>
                <div class="display-4">You are logged in</div>
            </Authorized>
            <NotAuthorized>
                <div class="display-4">Please login</div>
            </NotAuthorized>
        </AuthorizeView>

        <AuthorizeView Roles="Administrator">
            <div class="display-4">You are Administrator</div>
        </AuthorizeView>

        <AuthorizeView Roles="Moderator">
            <div class="display-4">You are Moderator</div>
        </AuthorizeView>

        <AuthorizeView Roles="User">
            <div class="display-4">You are a User</div>
        </AuthorizeView>

        <AuthorizeView Roles="Administrator, Moderator">
            <div class="display-4">You are an Administrator or a Moderator</div>
        </AuthorizeView>

    </div>
@code {

    private User User { get; set; } = new User();

    private async Task LoginUserRequest()
    {
        // validate user
        User.Role = new Random().Next(0, 2) == 0 ? UserRole.Administrator :
            new Random().Next(0, 2) == 0 ? UserRole.Moderator : UserRole.User;

        await ServiceProvider.Get<UserService>().LoginAsync(User);
        NavigationManager.NavigateTo("/");
    }

    private async Task LogoutUserRequest()
    {
        await ServiceProvider.Get<UserService>().LogoutAsync();
        NavigationManager.NavigateTo("/");
    }
}

Authorization/Authentication usage in action

The Roles mappings are only possible due to new Claim(ClaimTypes.Role, user.Role.ToString()) mapping within the UserService. Your roles may have different values, but the procedure equals the same.

<AuthorizeView>
    <Authorized>
        <p>The login I did</p>
    </Authorized>
    <NotAuthorized>
        <p>The login I must do</p>
    </NotAuthorized>
</AuthorizeView>

<AuthorizeView Roles="Administrator">
    <p>Yes, me Admin</p>
</AuthorizeView>

<AuthorizeView Roles="Moderator">
    <p>Yes, me Mod</p>
</AuthorizeView>

<AuthorizeView Roles="User">
    <p>Oh no, me user</p>
</AuthorizeView>

<AuthorizeView Roles="Administrator, Moderator">
    <p>Yes, we both nice</p>
</AuthorizeView>

4.8 8 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments