Building a Dynamic Console UI with .NET 10, MQTT, and Node-RED – Part 6: Securing the Pi Calculus Ecosystem with MQTT Authentication

Stylized Pi symbol surrounded by futuristic landscape

In Part 5, we evolved our dynamic console application into a versatile multi-client ecosystem. We seamlessly brought our “Qubit BBS” dashboard experience from the terminal to the browser using a new .NET 10 Blazor WebAssembly client (pi-wasm). We decoupled our core orchestration logic into the Pi.Shared library and routed customized UI configurations via Targeted Handshakes over MQTT WebSockets.

But as our Pi Calculus ecosystem grew, we realized a critical flaw: Our MQTT connections were entirely unauthenticated.

Anyone with a basic MQTT client could theoretically connect to our broker, intercept the dynamic Handshake topics, or inject unauthorized layout JSON into our applications. It was time to lock down our Node-RED backend and secure our .NET clients.

Mosquitto Password Protection

The first step was securing our Mosquitto MQTT broker. By default, Mosquitto allows anonymous connections. We updated our mosquitto.conf file to disable anonymous mode and point to a generated password file:

# Global Authentication Settings
allow_anonymous false
password_file /mosquitto/config/passwd

Next, we used the mosquitto_passwd utility to generate a secure user (pi_user) and a corresponding encrypted password hash. A quick restart of the Docker container, and our broker was officially sealed.

Of course, the immediate side-effect was that our poor pi-console and pi-wasm clients started throwing NotAuthorized exceptions! The brokers wouldn’t let them in to perform their Pi Calculus orchestration. We needed to teach our shared .NET 10 library how to handle credentials.

Teaching Pi.Shared to Authenticate

Inside our Pi.Shared library, we updated our central orchestration engine, the MqttService. We added new Username and Password properties to the class, and updated our runtime connection block to append .WithCredentials() to the MqttClientOptionsBuilder:

if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password))
{
    mqttClientOptionsBuilder = mqttClientOptionsBuilder.WithCredentials(Username, Password);
}

Now, the service was capable of authenticating. But where would it get those credentials? Hardcoding passwords directly into our Program.cs files or the shared library is a monumental security anti-pattern, especially since we track this project in a public GitHub repository.

We needed a strategy to load settings at runtime without ever letting Git see them. Because our clients run in two entirely different environments (a local terminal vs. a browser sandbox), we had to implement two distinct credential delivery mechanisms.

Securing the Terminal: secrets.json

For the native macOS terminal application (pi-console), we have full access to the local machine’s file system. We opted for a secrets.json file.

We created a .secrets folder at the root of our local workspace and added a secrets.json file containing our credentials:

{
  "MqttIpAddress": "localhost",
  "MqttPort": 9001,
  "Username": "pi_user",
  "Password": "super_secret_password"
}

Since the secrets.json file sits at the root of the solution, but the dotnet run execution happens deep inside the bin/Debug/net10.0/ folder, our MqttService needed to be smart enough to find it. We implemented a directory traversal loop to search upwards from the AppContext.BaseDirectory until it located the .secrets directory. Once found, it parses the JSON and populates the Username and Password fields.

Most importantly, we added .secrets/ to our .gitignore file, ensuring our Mosquitto passwords never accidentally get pushed to GitHub.

Securing the Browser: appsettings.json

Our Blazor WebAssembly client (pi-wasm) presented a completely different challenge. Since it runs inside the strict sandbox of your web browser, it has absolutely zero access to your local machine’s file system. It can’t directory-traverse its way to secrets.json.

Instead, Blazor WASM applications rely on configuration files served by the hosting web server. For pi-wasm, we created an appsettings.json file directly inside the wwwroot folder—the static web directory that gets bundled and sent to the browser:

{
    "Mqtt": {
    "Username": "pi_user",
    "Password": "super_secret_password"
    }
}

In pi-wasm/Program.cs, we tell our Dependency Injection container to pull the credentials directly from Blazor’s configuration builder:

service.Username = builder.Configuration["Mqtt:Username"];
service.Password = builder.Configuration["Mqtt:Password"];

When the user launches the website, the browser downloads appsettings.json, parses out the MQTT variables, injects them into the MqttService, and establishes an authenticated WebSocket connection.

Just like with the console app, we immediately added pi-wasm/wwwroot/appsettings.json to our .gitignore to protect the file from entering source control.

Connected and Secured

With both clients updated, our Pi Calculus orchestration system is fully authenticated. Node-RED requires a password to serve handshakes, and our .NET 10 UI clients dynamically inject their credentials based on their execution environment.

Whether you’re hitting the Qubit BBS from your local terminal or browsing it over WebSockets, the dynamic UI remains snappy, responsive, and—finally—secure.

Stay tuned as we continue expanding our MQTT UI orchestrator. You can follow along by cloning the pi-console Repo on GitHub!

Leave a Reply

Your email address will not be published. Required fields are marked *