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

Install dependencies

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

Install-Package AuthenticatedEncryption -Version 1.0.1
Install-Package Blazored.SessionStorage -Version 1.0.11
  • AuthenticatedEncryption: We don’t want to slap our session object into the user’s face, without even pretending to know what we are doing.
  • SessionStorage: 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.AddAuthorization();
services.AddAuthentication();
services.AddBlazoredSessionStorage();
services.AddScoped<AuthenticationStateProvider, UserService>();

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 byte[] authenticationKey, cryptographyKey;
        private ISessionStorageService storageService;

        public UserService(ISessionStorageService storageService)
        {
            this.storageService = storageService;

            authenticationKey = AuthenticatedEncryption.AuthenticatedEncryption.NewKey();
            cryptographyKey = AuthenticatedEncryption.AuthenticatedEncryption.NewKey();
        }

        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()
        {
            string encryptedObj = await storageService.GetItemAsync<string>(USER_SESSION_OBJECT_KEY);

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

            string decryptedObj = AuthenticatedEncryption.AuthenticatedEncryption.Decrypt(encryptedObj, cryptographyKey, authenticationKey);

            try
            {
                return JsonConvert.DeserializeObject<User>(decryptedObj);
            }
            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) => await storageService.SetItemAsync(USER_SESSION_OBJECT_KEY, 
            AuthenticatedEncryption.AuthenticatedEncryption.Encrypt(JsonConvert.SerializeObject(user), cryptographyKey, authenticationKey));

        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

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">
        <div class="col-md-auto">

            <EditForm OnValidSubmit="LoginUserRequest" Model="User">
                <FluentValidator TValidator="UserLoginValidation" />

                <div class="form-group">
                    <InputText placeholder="Email-Adresse" @bind-Value="User.EmailAddress" class="form-control" />
                    <ValidationMessage For="@(() => User.EmailAddress)" />
                </div>

                <div class="form-group">
                    <InputText type="password" placeholder="Passwort" @bind-Value="User.Password" class="form-control" />
                    <ValidationMessage For="@(() => User.Password)" />
                </div>
            </EditForm>

        </div>
    </div>

</div>

@code {

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

    private async Task LoginUserRequest()
    {
        // validate user

        await ServiceProvider.Get<UserService>().LoginAsync(User);
        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.6 5 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments