Generating CryptoJS compatible HMAC SHA256 hashes in C#
Generating CryptoJS compatible HMAC SHA256 hashes in C#

Feel like sharing?

(or Creating NodeJS Crypto Compatible HMAC SHA256 Hashes)

If you’ve attempted to generate hashes that are compatible with the NodeJS Crypto library, specifically its HMAC sha256 method used by several SaaS companies to authenticate, you might have stumbled upon a few obstacles. Today, I want to share with you a recent experience I had while integrating with HyvorTalk, a popular commenting system, and how I managed to convert the NodeJS Crypto HMAC sha256 compatible code to C#.

Goal

Our goal here is to authenticate HyvorTalk users via Single Sign-On (SSO) from our own application. HyvorTalk SSO requires the generation of an HMAC SHA256 hash to accomplish this. Once this SSO is successfully integrated, we can embed the HyvorTalk script on the page and users will be able to generate HvyorTalk comments without having to log in separately to the HyvorTalk application.

Razor Page

Let’s start with the Razor page setup, which is straightforward. We include the HyvorTalk embed.js script and use a conditional statement to check if a user is authenticated. If they are, we embed a HyvorTalk comment element.

@page 

@model Web.Pages.HyvorTestViewModel

/* inject whatever authentication mechanism your app is using */
@inject ICurrentUser CurrentUser

<script async src="https://talk.hyvor.com/embed/embed.js" type="module"></script>

@if (CurrentUser.IsAuthenticated)
{
    <hyvor-talk-comments website-id="@Model.HyvorSiteId"
                         page-id="@Model.HyvorPageId"
                         sso-user='@Model.HyvorUserData'
                         sso-hash='@Model.HyvorHash'>
    </hyvor-talk-comments>
}
                

The HyvorTalk comment element embeds a number of tag properties which get their value from the ViewModel.

The most challenging property to get was the sso-hash and generating something that produced the same results as the NodeJS Crypto HMAC sha256 library.

ViewModel / Code behind

In the code-behind, we set up our page model with properties for HyvorSiteId, HyvorUserData, HyvorHash, and HyvorPageId.

using System;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace Web.Pages;

public class HyvorTalkUser
{
    public string id { get; set; }
    public string name { get; set; }
    public string timestamp { get; set; }
    public string email { get; set; }
}

public class HyvorTestViewModel : PageModel
{
    
    public string HyvorSiteId {get; set; }
    public string HyvorUserData { get; set; }
    public string HyvorHash { get; set; }
    public string HyvorPageId { get; set; }

    private IConfiguration _configuration;
    public HyvorTestViewModel(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    
    public async Task<IActionResult> OnGetAsync(int id, string slug)
    {
        string privateKey = _configuration["Hyvor:PrivateKey"];
        HyvorSiteId = _configuration["Hyvor:SiteId"];
        HyvorPageId = "my-unique-page-id";

        // use whatever mechanism required to determine if the user is logged in.
        if (CurrentUser.IsAuthenticated)
        { 
            var dto = new DateTimeOffset(DateTime.UtcNow);
            string unixTimeStamp = dto.ToUnixTimeSeconds().ToString();

            HyvorTalkUser hyvorUser = new HyvorTalkUser
            {
                id = CurrentUser.Id.ToString(),
                name = CurrentUser.Name,
                timestamp = unixTimeStamp,
                email = CurrentUser.Email
            };

            var json = JsonSerializer.Serialize(hyvorUser);
            var valueBytes = System.Text.Encoding.UTF8.GetBytes(json);
            var base64String = System.Convert.ToBase64String(valueBytes);
            string hash = CalculateHMACSHA256(base64String, privateKey);
            
            HyvorUserData =  base64String;
            HyvorHash = hash;
        }

        return Page();

    }

 
    private string CalculateHMACSHA256(string data, string privateKey)
    {
        var encoding = new System.Text.UTF8Encoding();
        byte[] keyBytes = encoding.GetBytes(privateKey);
        byte[] dataBytes = encoding.GetBytes(data);
        using (var hmacsha256 = new HMACSHA256(keyBytes))
        {
            byte[] hashBytes = hmacsha256.ComputeHash(dataBytes);
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }
    }
}

When the user visits the page (OnGetAsync), we fetch our Hyvor site id and private key from the configuration. If the user is authenticated, we construct a HyvorTalkUser object, serialize it to JSON, and then convert it to a Base64 encoded string. This encoded string is then hashed using the CalculateHMACSHA256 method. The resultant hash, alongside the base64 encoded user data, is used for SSO authentication.

The CalculateHMACSHA256 method is where the magic happens. Here, we convert the data and the private key to byte arrays and use them to create a new HMACSHA256 instance. By invoking the ComputeHash method on this instance, we obtain our hashed bytes, which are then converted back to a string to generate the HMAC SHA256 hash compatible with the NodeJS Crypto library.

This process was not straightforward, and it took some time to figure out how to handle the HMAC SHA256 hashing in C#. The major challenge was ensuring the output was exactly as it would be with NodeJS Crypto, and this involved proper string conversion and formatting.

HyvorTalk support helped debug the JS side of this with me. Kudos to them!

In conclusion, it was almost impossible to find the proper conversion algorithms online. The internet (StackOverflow in particular) is jam packed full of wrong answers. With a lot of trial and error, a little help from the HyvorTalk team, and some AI assistance, I finally got a working implementation.

Now I wonder if anyone is actually going to talk / comment on the site once I bring my new side hustle to life.

Happy coding!

Feel like sharing?

Last modified: July 30, 2023

Author