Use protected browser storage in Blazor
In the previous article we learned to use sessionStorage and localStorage in Blazor apps. The localStorage object also has storage event that one can use to get notified when localStorage is modified in some way. In this article we will first discuss the storage event with an example. We will then learn to use what is known as Protected Browser Storage that is available exclusively for Blazor Server apps.
The localStorage object has storage event. This event is bit tricky in that it doesn't fire for the page modifying the localStorage; rather it is intended for other pages to get notified when the storage changes in some way.
To understand this behavior, let's create another razor component -- Index2.razor. This newly added component will subscribe to the storage event. Whenever Index.razor changes the localStorage, Index2.razor will get the notification and will display a message to the end user.
As you can see from the above figure, Index2.razor shows the key that was changed, its old value and its new value.
To begin developing this example, open the same project that you created in the previous example. Then add a new Razor Component named Index2.razor in the Pages folder.
Open Index2.razor and set it's route using the @page directive.
@page "/index2"
Then inject IJSRuntime into the component because we need JS interop in this component also.
@inject IJSRuntime Js
Then add the following code into the @code block.
public string Message{ get; set; }
public async Task OnButtonClick()
{
await Js.InvokeVoidAsync
("WireStorageHandler",
DotNetObjectReference.Create(this));
}
[JSInvokable]
public void NotifyStorageChanged
(string key, string oldVal, string newVal)
{
Message = $"Key {key} changed
from '{oldVal}' to '{newVal}'";
StateHasChanged();
}
Notice this code carefully. We define Message property to display a message to the user. The OnButtonClick() method acts as an event handler to the Wire Storage Event button. Inside, we call InvokeVoidAsync() method to invoke a JavaScript function named WireStorageHandler(). We will write this function shortly. Note that we also pass a reference to the Index2 component to the WireStorageHandler() function because WireStorageHandler() needs to setup a notification mechanism back to our Index2 component. This is done using DotNetObjectReference. If you are not familiar to Blazor's JS interop consider reading this, this, and this.
Once the storage event handler is triggered we need to notify the Index2 component of the event. To accomplis this we write NotifyStorageChanged() method. Note that this method is decorated with [JSInvokable] attribute and accepts three parameters -- key, oldVal, and newVal. Inside, it simply forms a message that displays the key name, its old value and its new value.
Now add the following markup below the @code block.
<h2>Local Storage Event Handling Demo</h2>
<h3>@Message</h3>
<button @onclick="@OnButtonClick">
Wire Storage Event</button>
Next, add a new JavaScript file named StorageEventHandler.js inside the wwwroot folder.
Then write the following code in StorageEventHandler.js file.
function WireStorageHandler(instance) {
window.addEventListener("storage", (evt) => {
instance.invokeMethodAsync("NotifyStorageChanged",
evt.key, evt.oldValue, evt.newValue)
.then((result) => {
console.log(result);
});
});
}
The WireStorageHandler() function wires a storage event handler using addEventListener() method. Inside, we notify the change made to localStorage back to the Index2 component. This is done using invokeMethodAsync() method. The invokeMethodAsync() method takes the name of the C# method to call and a series of method parameters. In our example we are calling NotifyStorageChanged() method. We pass the key that was changed, its old value, and its new value.
After writing this code save StorageEventHandler.js file and add a <script> element in the _Host.razor component just above the </body> tag.
<script src="/StorageEventHandler.js"></script>
<script src="_framework/blazor.server.js"></script>
This completes Index2.razor component. Let's check its functioning.
Run the application and make sure Index.razor is loaded in the browser as shown below:
Now open a new tab and navigate to the Index2.razor component.
Click on the Wire Storage Event button on Index2.razor component.
Then switch to Index.razor component, pick Local Storage radio button, and enter a key and its value. As an example, enter key1 as the key name and Hello World as its value.
Click on the Set Value button from Index.razor. Since you are adding a new key to localStorage, Index2.razor gets a notification about this change. And a message is displayed like this:
Since key1 is new key (it didn't exist before), its old value is shown as an empty string. Now change key1 again from Hello World to Hello Universe. This time your message will be like this:
In the two example developed so far, we stored plain string data in the sessionStorage and localStorage objects. The downside of this approach is that the end users can easily see what's stored using browser tools. They can even change the values easily from the browser.
Although web storage is not intended to persist sensitive data, it is good idea to encrypt the values before they are persisted in the sessionStorage and localStorage objects.
A very simple and basic technique is to Base64 encode and decode the data while persisting it into the storage. Although, Base64 is not a strong encryption algorithm, it can be a quick and easy way to hide the plain text values. And it works with Blazor Server as well as Blazor Web Assembly apps.
The following code show the OnSetClick() and OnGetClick() methods that use base64 encoding and decoding.
public async Task OnSetClick()
{
var base64 = Convert.ToBase64String
(Encoding.UTF8.GetBytes(FormData.Value));
await Js.InvokeVoidAsync
($"{GetStorageType()}.setItem",
FormData.Key, base64);
}
public async Task OnGetClick()
{
var base64 = await Js.InvokeAsync
<string>($"{GetStorageType()}.getItem",
FormData.Key);
FormData.Value = Encoding.UTF8.GetString
(Convert.FromBase64String(base64));
}
The following figure shows how the keys are now stored in Base64 format.
For Blazor Server apps there is a better option available -- Protected Browser Storage. The Protected Browser Storage uses ASP.NET Core Data Protection for web storage and is a more secure mechanism than plain strings or Base64 strings.
To use Protected Browser Storage you need to inject ProtectedSessionStorage and ProtectedLocalStorage in the Index.razor component. So, add the following to Index.razor :
@inject ProtectedSessionStorage Pss
@inject ProtectedLocalStorage Pls
The ProtectedSessionStorage class encapsulates browser's sessionStorage whereas ProtectedLocalStorage class encapsulates browser's localStorage. These classes have methods such as GetAsync(), SetAsync(), and DeleteAsync() for doing the respective job.
Your OnSetClick(), OnGetClick(), and OnRemoveClick() methods will need to be changed to use Protected Browser Storage objects. The modified methods are shown below:
public async Task OnSetClick()
{
if (FormData.StorageType ==
WebStorageType.SessionStorage)
{
await Pss.SetAsync
(FormData.Key, FormData.Value);
}
if (FormData.StorageType ==
WebStorageType.LocalStorage)
{
await Pls.SetAsync
(FormData.Key, FormData.Value);
}
}
public async Task OnGetClick()
{
if (FormData.StorageType ==
WebStorageType.SessionStorage)
{
var result = await
Pss.GetAsync<string>(FormData.Key);
FormData.Value = result.Value;
}
if (FormData.StorageType ==
WebStorageType.LocalStorage)
{
var result = await
Pls.GetAsync<string>(FormData.Key);
FormData.Value = result.Value;
}
}
public async Task OnRemoveClick()
{
if (FormData.StorageType ==
WebStorageType.SessionStorage)
{
await Pss.DeleteAsync(FormData.Key);
}
if (FormData.StorageType ==
WebStorageType.LocalStorage)
{
await Pls.DeleteAsync(FormData.Key);
}
}
Notice the code shown in bold letters. The OnSetClick() method uses SetAsync() method of ProtectedSessionStorage or ProtectedLocalStorage depending on the radio button selection. The SetAsync() method accepts a key name and its value. The following figure shows how a key is stored in the sessionStorage object.
As you can see, the value is now encrypted and then persisted in the web storage. To read this value you need to call GetAsync() as we have done inside the OnGetClick() method.
In all the examples discussed so far we persisted simple string data to the web storage. What if you want to persist objects? In that you case you need to first serialize the object into some text form (such as JSON or XML) and then save the data to the web storage.
Suppose you have an Employee class and its object like this :
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
};
var emp = new Employee
{
EmployeeId = 1,
FirstName = "Nancy",
LastName = "Davolio"
};
To save this Employee object to web storage we can convert it into JSON format like this:
var json = JsonSerializer.Serialize<Employee>(emp);
The resultant JSON can be saved to the web storage as before:
await Js.InvokeVoidAsync
("sessionStorage.setItem", "key1", json);
Here, we persisted the JSON serialized Employee in sessionStorage.
To retrieve key1 back into your code you will write:
var json = await Js.InvokeAsync<string>
("sessionStorage.getItem", "key1");
var emp = JsonSerializer.Deserialize
<Employee>(json);
Of course, you can use Protected Browser Storage (if it's Blazor Server app) to encrypt this JSON data as discussed earlier.
That's it for now! Keep coding!!