[Introduction to Blazor] Blazor beginners tried to deploy with login to chat function ~ Part 4 ~

7 minute read

Last time

[Introduction to Blazor] Blazor beginners tried to deploy with login to chat function ~ Part 1 ~
[Introduction to Blazor] Blazor beginners tried to deploy with login and chat functions-Part 2-
[Introduction to Blazor] Blazor beginners tried to deploy with login to chat function ~ Part 3 ~

It is a continuation of.

Last time, I made it possible to log in only to users who are registered using Cognito.
I still have a problem with this, but before that, I would like to keep the login information.

Retain login information

I think that many services retain login information by the following processing.
This time, I will add functions according to this method.
image.png

Source: Is security measures perfect? Introducing the difference between sessions and cookies and how to use them in PHP!

Reading and writing cookies with Blazor

I did a lot of research, but so far I haven’t found any information about cookies.

Blazor can’t handle HTTPContext (?)

In .NET MVC Core etc., it was possible to handle sessions and cookies by passing through HTTPContext, but it can only be handled from Controller class. It’s the same with Blazor, but I thought it was a bit strange to add the Controller class to the current solution (or what does that mean for Blazor?), So it’s different. I thought about the method.

Handle cookies with JavaScript

Another way I thought was to handle cookies with JavaScript. It feels like it spoils the goodness of Blazor, but it can’t be helped here. Let’s implement it.

Add the process of JS you want to call

Add the WriteCookie method and ReadCookie method to the _Host.cshtml file.

_Host.cshtml


<script>
    window.blazorExtensions = {
        WriteCookie: function(name, value) {
            var maxAge = "max-age=3600; ";
            var path = "path=/; ";
            document.cookie = name + "=" + value + "; " + maxAge + path;
        },

        ReadCookie: function () {
            return document.cookie;
        },
    }
</script>

Addictive point

Here, I wanted to return the ReadCookie method as an associative array instead of the entire cookie, but I was crying because I got a ʻUnHundled Error` just by generating the array. Please let me know if there is a way to return an array.

Call JavaScript processing from C

Newly added Service folder and CookieService class.
Because I thought it might not be just a particular page that I wanted to do with cookies. It’s not a class that instantiates multiple times, and it’s a smart idea to let DI use it on any page.
image.png

The code is below.

CookieService.cs


using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace LoginTest.Service
{
    public class CookieService
    {
        #region field
        private readonly IJSRuntime _jsRuntime;
        #endregion

        #region constructor
        public CookieService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }
        #endregion

        #region method
        /// <summary>
        ///Write to cookie
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public async Task WriteCookieAsync(string key, string value)
        {
            await _jsRuntime.InvokeVoidAsync("blazorExtensions.WriteCookie", key, value).ConfigureAwait(false);
        }
        /// <summary>
        ///Reading cookies
        /// </summary>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> ReadCookieAsync()
        {
            var cookieDictionary = new Dictionary<string, string>();
            var cookie = await _jsRuntime.InvokeAsync<string>("blazorExtensions.ReadCookie").ConfigureAwait(false);

            var cookieSplit = cookie.Split(";");
            for (var i = 0; i < cookieSplit.Length; ++i)
            {
                var cookieKeyValue = cookie.Split("=");
                cookieDictionary.TryAdd(cookieKeyValue[0], cookieKeyValue[1]);
            }

            return cookieDictionary;
        }
        #endregion
    }
}

It can be executed simply by passing the method name and arguments of the JavaScript declared earlier to the ʻInvokeAsync method of the ʻIJSRuntime class. Easy and easy.

All you have to do is add the CookieService class to the ConfigureServices method of the Startup class with Scoped so that you can DI.

Startup.cs


public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
    services.AddBlazoredLocalStorage();
    services.AddScoped<CookieService>(); //★ Add this line ★
}

Addictive point

The implementation looks very easy, but it took me a long time to get to this process. There are two addictive points.

** ① DI to service class can only be constructor injection **
Until now, if you somehow added the ʻInject attribute, you could DI for the service, but it seems that it was because it was a razor file or arazor.cs file`.

You can read about it on the following page.
Use DI for services

So this CookieService class does constructor injection and injects into ʻIJSRuntime class. Even if you declare the ʻIJSRuntime class in the property and add the ʻInject attribute, it will remain null` and no value will be entered.


** ② If you want to DI the default service in the service class, add the service according to the validity period of the default service **
It seems that many people don’t understand what they are saying, so I will explain it carefully.

The service class is the CookieService class that we are trying to create this time.
The default service refers to the following three services. These are services that can be used from the beginning.

image.png

This time I’m creating a solution on the server side.
I also want to use the default service ʻIJSRuntime class. As you can see from the image above, the validity period of the ʻIJSRuntime class is “scope” on the server side.
So, as shown below, CookieService class is added by” scope “.

Startup.cs


services.AddScoped<CookieService>(); //★ Add this line ★

I didn’t know I needed to fit this, so I initially added it as a singleton. Then, the following error was output.

キャプチャ.JPG

InvalidOperationException: Cannot consume scoped service ‘Microsoft.JSInterop.IJSRuntime’ from singleton ‘LoginTest.Service.CookieService’.
(InvalidOperationException: The scope service “Microsoft.JSInterop.IJSRuntime” cannot be used from the singleton “LoginTest.Service.CookieService”.)

Here’s an explanation of how long the service is valid. The scope is valid only on the server side, isn’t it?
image.png

Reuse login information

We use cookies and local storage to retain your login information. Also. If your login information is saved, let’s automatically route it to the chat page.

First, install Blazored.LocalStorage from NuGet.
image.png

Using this library makes it very easy to use local storage. If it is local storage, you can close the browser or take over the saved information in another tab, so this time we will use local storage.
image.png
Source: Active engineers explain how to use JavaScript sessionStorage [for beginners]


Let’s recall the figure below again.
image.png

Here’s what to do when a user visits a page:

  1. Access the login page
  2. Get the session ID stored in the user’s cookie
  3. Find the session ID held in local storage that matches the session ID of the cookie
  4. Get the last login information held in the local storage
  5. Route to chat page

If the session ID is not saved in the cookie, create it and let the local storage hold the login information using the session ID as a key when login is successful.


Since HTTPContext cannot be used, Session ID is replaced by New Guid.
The above processes 1 to 5 could be implemented by the following processes.

C#:Index.razor.cs


protected override async Task OnAfterRenderAsync(bool firstRender)
{
    //Feels like the IsPostback property?
    if (firstRender)
    {
        //If you don't have a session ID, embed it in a cookie
        var newSessionID = Guid.NewGuid().ToString();
        var cookie = await CookieService.ReadCookieAsync().ConfigureAwait(false);
        if (cookie == null)
        {
            await CookieService.WriteCookieAsync(SessinID, newSessionID).ConfigureAwait(false);
        }
        else
        {
            var sessionID = cookie.FirstOrDefault(x => x.Key == SessinID);
            if (sessionID.Key == null)
            {
                //If you don't have a session ID, embed it in a cookie
                await CookieService.WriteCookieAsync(SessinID, newSessionID).ConfigureAwait(false);
            }
            else
            {
                //If you have a session ID and you also have login information in your local storage
                var loginData = await LocalStorage.GetItemAsync<LoginData>(sessionID.Value).ConfigureAwait(false);
                if (loginData != null)
                {
                    Navigation.NavigateTo("Chat", false);
                }
            }
        }
    }
}

Also, if you can log in as the correct user without checking Validation, add the login information to the local storage and you’re done. Since the cookie may have been deleted during this time, we have added a process to write to the cookie just in case.

C#:Index.razor.cs


public async Task OnValidSubmit(EditContext context)
{
    Console.WriteLine($"OnValidSubmit()");

    var errorMessage = await SignInUserAsync().ConfigureAwait(false);
    if (string.IsNullOrEmpty(errorMessage))
    {
        var cookie = await CookieService.ReadCookieAsync();
        var sessionID = cookie.FirstOrDefault(x => x.Key == SessinID);
        if (sessionID.Key == null)
        {
            //If you don't have a session ID, embed it in a cookie
            var newSessionID = Guid.NewGuid().ToString();
            await CookieService.WriteCookieAsync(SessinID, newSessionID).ConfigureAwait(false);
            sessionID = cookie.Single(x => x.Key == SessinID);
        }
        await LocalStorage.SetItemAsync(sessionID.Value, LoginData).ConfigureAwait(false);

        Navigation.NavigateTo("Chat", false);
    }
    else
    {
        LoginErrorMessage = errorMessage;
    }
}

Try to run

You can glimpse the login screen, but you can see that it will be routed to the chat page immediately.
Counter.gif

I think the login screen is probably due to routing with the ʻOnAfterRenderAsync method. However, there is no ʻOnBeforeRender method.

However, I received a pull request, so it will be available in the future.
OnBeforeRender #1716

Summary

In the previous articles, it was Full C #, but I ended up using JavaScript to handle cookies. Probably, in the future, I think that libraries will come out for parts other than C #, so I would like to expect it.

Next time, I would like to solve the problem that I can route to the chat page without authentication.

** Referenced page **

-Is security measures perfect? Introducing the difference between sessions and cookies and how to use them in PHP!
-Insert ASP.NET Core Blazor Dependency
-SPA with Blazor! (7) –DI (Dependency Injection) –Official version supported