Sep 25, 2024

Blockchain + .NET MAUI ❤️ An experience working with authentication

This blog discusses integrating blockchain authentication using Ethereum with a .NET MAUI app, focusing on secure login with WalletConnect and the SIWE protocol. It offers insights into decentralized authentication and technical challenges faced during development.
Image UXDIvers blog

Overview

In recent years, blockchain technology has revolutionized various industries by providing decentralized, secure, and transparent solutions. As businesses increasingly seek to leverage blockchain's potential, integrating it with modern development frameworks like .NET MAUI becomes essential. However, .NET MAUI integration with the Ethereum ecosystem is still in its infancy and there is little experience working with these two technologies. This article explores our experience in using blockchain for authentication within a .NET MAUI application, focusing on the integration of the "Sign-In with Ethereum" (SIWE) protocol. We delve into the process of connecting a MAUI app with Ethereum wallets using WalletConnect, highlighting how this approach enhances security and user experience by allowing users to authenticate without relying on traditional credentials.

Introduction

Blockchain is a technology that was created in 2008 as the foundation of Bitcoin, the first cryptocurrency. Essentially, it is an append-only database, meaning that data can be added but not deleted or modified, thus ensuring its immutability. This distributed ledger system is maintained through a network of decentralized nodes, eliminating the need for intermediaries and reducing the risk of manipulation or censorship. Additionally, blockchain offers a degree of pseudonymity, as transactions and data are associated with cryptographic addresses rather than personal identities, protecting user privacy while maintaining the system's transparency.

Initially, blockchains were designed to support cryptocurrencies like Bitcoin, providing a secure and decentralized system for conducting financial transactions. However, over time, blockchain technology has evolved and expanded its scope, finding applications in various sectors beyond finance, such as supply chain management, health, art, and digital identities. Today, blockchains are also used for authentication, allowing individuals to verify their identity securely and in a decentralized manner, eliminating the need to rely on traditional intermediaries and enhancing privacy and control over personal data.

The area of authentication with blockchain is very broad and includes advanced methods such as anonymous credentials. These credentials allow for identity verification without revealing personal information by using cryptographic techniques like zero-knowledge proofs. This ensures secure and private transactions, eliminating the need for intermediaries and allowing users to maintain control over their personal data in decentralized digital environments. These credentials, for example, can be used to overcome challenges in interoperating permissioned and permissionless blockchains, as explained in this article I co-authored with other colleagues.

Challenge

In this article, we will use the Ethereum blockchain to authenticate a user in our .NET MAUI app using their wallet. This concept is based on ERC-4361: Sign-In with Ethereum. SIWE is a protocol that allows users to log in to web applications using their Ethereum addresses, eliminating the need for traditional usernames and passwords. By signing a message with their private key, users can demonstrate ownership of an Ethereum address without exposing their private keys. This provides a secure and decentralized authentication method, enhancing user privacy and security by avoiding reliance on centralized and vulnerable authentication systems. This mechanism is particularly well-suited for web3 applications compared to traditional methods like OpenID Connect or OAuth2.

There are thousands of wallets on the market, and implementing each of their SDKs to connect our app with every possibility is unfeasible. Therefore, there are two options: 1) select only the most used wallets, such as TrustWallet, Metamask, etc, and 2) use WalletConnect (option we will follow in the article). 

WalletConnect is an open-source protocol that securely connects decentralized applications (dApps) with wallets, allowing users to authorize transactions by scanning a QR code or using deep links without compromising their private keys. In this case, we will use WalletConnectSharp, which is an implementation of the protocol in C#. You can check its GitHub here

Solution

Blockchain Android demo image

We are going to build an example application using .NET MAUI that allows users to sign in with their Ethereum wallet. The application will feature a sign-in button that implements the Sign-In with Ethereum (SIWE) protocol. The first step is to visit the WalletConnect Cloud website, sign in, and create an account if you don't have one. After logging in, create a new project on the platform and be sure to save the Project ID, as it will be required later.

Next, create a new class that inherits from Button and add a command to handle the click event, like this:

public WalletConnectButton()
{   
    Text = "Sign in with WalletConnect";   
  
    Command = new Command(OnClicked);
}


The OnClicked function contains the high-level logic to perform the sign-in process. It involves connecting the app to the wallet, creating the sign-in message, requesting the wallet to sign that message, and finally verifying that the signature is valid. We will go through this process step by step.

private async void OnClicked()
{
   //Connects the app with the wallet.
   var connected = await ConnectWallet();
   if (!connected)
   {
       return;
   }
  
   //User's wallet address
   var address = _dappClient.AddressProvider.DefaultSession.CurrentAddress(CHAIN_ID).Address;
      
   //Return the siweMessage and the signature
   var (signature, siweMessage) = await SignInWithEthereum(address);


   if (signature == null || siweMessage == null)
   {
       var errorMsg = "Error: An error occurred signing in with ethereum";
       Debug.WriteLine(errorMsg);
       ErrorCommand?.Execute(errorMsg);
       return;
   }


   //Verifies that the address that signs the SIWE message is the user's address.
   var isValid = VerifyPersonalSignSignature(siweMessage.ToString(), signature, address);
   if (isValid)
   {
       Debug.WriteLine("Success: User authenticated with Ethereum");
       SignedInCommand?.Execute(new AutenticatedUserData
       {
           AuthenticationNonce = siweMessage.Nonce,
           UserAddress = address,
           WalletName = _dappClient.AddressProvider.DefaultSession.Peer.Metadata.Name,
           UserPublicKey = _dappClient.AddressProvider.DefaultSession.Peer.PublicKey
       });
       return;
   }
      
   ErrorCommand?.Execute("The wallet signature is not valid");
   Debug.WriteLine("Error: The wallet signature is not valid");
}

Wallet Connection

Let's take a closer look at the ConnectWallet function. This function constructs two objects: SignClientOptions and ConnectOptions. These objects can be modified to customize the message displayed by the wallet and adjust some connection settings.

private async Task<bool> ConnectWallet()
{
   //Modify this function to customize the client options.
   var dappOptions = CreateSignClientOptions();


   //Modify this function to customize the connection options.
   var dappConnectOptions = CreateConnectionOptions();


   //Initiates the client
   _dappClient = await WalletConnectSignClient.Init(dappOptions);
   var connectData = await _dappClient.Connect(dappConnectOptions);


   //Launch the wallet app with the connection params
   //On android the OS will let you choose the wallet app but on iOS it won't
   var uri = new Uri(connectData.Uri);
   await Launcher.Default.OpenAsync(uri);
  
   try
   {
       //Wait until the user approves the connection
       //If the connection is not approved or it times out an exception will be thrown
       await connectData.Approval;
      
       Debug.WriteLine("Approved");
       return true;
   }
   catch (Exception e)
   {
       Debug.WriteLine(e.Message);
       ErrorCommand?.Execute(e.Message);
   }


   return false;
}


In the SignClientOptions object, you must set the ProjectId using the ID obtained from the WalletConnect website. You can also uncomment the Storage property to enable persistent storage. The Metadata object allows you to customize the appearance of the wallet message. 

Here is how you can set up the SignClientOptions object:

return new SignClientOptions()
{
   ProjectId = WALLET_CONNECT_PROJECT_ID,
   Metadata = new Metadata()
   {
       Description = "MAUI WalletConnect and SIWE example",
       Icons = new[]
       {
           "https://cdn.prod.website-files.com/630fae9a46ee72ef23481b76/643471939ca472a0f0fa96b6_favicon.png"
       },
       Name = "MAUI SIWE Example",
       Url = "https://uxdivers.com/"
   },
   Storage = new InMemoryStorage()
};


Below is an example of how the message will look with these customizations:

Customized wallet message


The ConnectOptions object specifies the expiration time of the connection and the required namespaces, which are the primitives that the wallet must support to establish the connection. Although the personal_sign method is the only essential method for our needs, we include additional methods in this example to provide a more comprehensive illustration of potential functionalities.

private ConnectOptions CreateConnectionOptions()
{
   return new ConnectOptions()
   {
       Expiry = (long)new TimeSpan(0,10,0).TotalSeconds,
       RequiredNamespaces = new RequiredNamespaces()
       {
           {
               "eip155", new ProposedNamespace()
               {
                   Methods = new[]
                   {
                       "eth_sendTransaction", "eth_signTransaction", "eth_sign", "personal_sign", "eth_signTypedData",
                   },
                   Chains = new[]
                   {
                       CHAIN_ID
                   },
                   Events = new[]
                   {
                       "chainChanged", "accountsChanged", "connect", "disconnect"
                   }
               }
           }
       }
   };
}


Once we have created the SignClientOptions and ConnectOptions objects, we can initialize the client, an instance of WalletConnectSignClient. This object manages the communication between the app and the wallet. After initializing it, we call the Connect method to obtain the ConnectData object. In this case, we use a deep link provided by ConnectData to open the wallet app on the device. Alternatively, you can display a QR code on the screen for the user to scan with their phone. This second approach is preferable when the user is conducting the transaction on a terminal and has the wallet on a different device. For this example, we use the deep link to open the app with the Launcher, a class provided by MAUI to launch other applications.

Finally, the ConnectWallet method waits until the wallet approves the connection. If the user doesn't approve the connection or if the connection times out, an exception is thrown.

Siwe Request

Now that we have established a connection with the wallet, it is time to make the sign-in request. The SignInWithEthereum function is responsible for this task. It begins by creating an instance of the SiweMessage class.

private async Task<(string, SiweMessage)> SignInWithEthereum(string address)
{
   //Creates the SIWE message that will be signed by the user's wallet
   var siweMessage = CreateSiweMessage();


   //Create the request following the RPC format for 'personal_sign'
   var request = new EthPersonalSign(siweMessage.ToString(), address);


   //Launches the wallet app
   Application.Current.Dispatcher.Dispatch(async () => await Launcher.Default.OpenAsync($"wc:"));
  
   string signature;
   try
   {
       //Send the sign request to the user's wallet with the siwe message
       signature = await _dappClient.Request<EthPersonalSign, string>(_dappClient.AddressProvider.DefaultSession.Topic, request, CHAIN_ID, siweMessage.Expiry);
   }
   catch (Exception e)
   {
       signature = null;
       siweMessage = null;
   }
  
   return (signature, siweMessage);
}


The SiweMessage class represents a "Sign-In with Ethereum" message that is used to authenticate users by verifying their control over a particular Ethereum address. This class includes various properties such as Domain, Address, Statement, Aud (audience), ChainId, Nonce, Expiry, Version, and Iat (issued at time). The Nonce and Expiry parameters play crucial roles in security by preventing replay attacks and ensuring that the signed message is only valid for a limited period. The protocol could be improved by incorporating additional security measures, such as specifying the allowed time window for the signature's validity and implementing stricter validation of the Domain and Aud fields to prevent phishing attacks.

public class SiweMessage
{
   public string Domain { get; set; }
   public string Address { get; set; }
   public string Statement { get; set; }
   public string Aud { get; set; }
   public string ChainId { get; set; }
   public string Nonce { get; set; }
   public long? Expiry { get; set; }
   public string Version => "1";
   public string Iat { get; } = DateTime.Now.ToISOString();
}


The next step is to define the request. In this case we are going to use the ‘personal_sign’ method. In the WalletConnect docs Ethereum | WalletConnect Docs there is a summary of the Ethereum RPC Methods and how to make the request and what are the responses of each method. Using the personal_sign method to sign "Sign-In with Ethereum" (SIWE) messages is the standard approach in the industry due to its user-friendly format and enhanced security. This method allows users to sign a clear, readable message, reducing the risk of accidental misuse and making it clear that they are not signing a transaction. Its widespread support among Ethereum wallets ensures compatibility and a consistent user experience. Additionally, the added prefix helps prevent replay attacks by clearly distinguishing signed messages from transactions.

As explained in the documentation, we define the EthPersonalSign class to follow the RPC for the personal_sign method. This class receives the message and the user address as parameters in its constructor. Since the JSON converter requires a parameterless constructor, an additional constructor was added to accommodate this requirement.

[RpcMethod("personal_sign"), RpcRequestOptions(Clock.ONE_MINUTE, 99998)]
public class EthPersonalSign : List<string>
{
   public EthPersonalSign(string message, string address) : base(new List<string> { message, address })
   {
      
   }


   public EthPersonalSign()
   {
      
   }
}

Finally, the method reopens the wallet using the Launcher class and sends the request to the wallet. The response contains the user's signature of the message, which is later returned by the function as a pair with the original SIWE message. This information will be used by the VerifyPersonalSignSignature function to validate the signature.

Validation

The VerifyPersonalSignSignature function checks whether a given Ethereum signature was created by a specific Ethereum address using the Nethereum https://nethereum.com/ library. It takes a message, a signature, and an expected address as inputs. The function uses an EthereumMessageSigner to encode the message in UTF-8 and recover the signing Ethereum address from the signature using elliptic curve recovery. It then compares this recovered address to the expected address in a case-insensitive manner. If they match, the function returns true, confirming that the signature is valid and was generated by the expected address owner; otherwise, it returns false, indicating that the signature is invalid or from a different address.

public bool VerifyPersonalSignSignature(string message, string signature, string expectedAddress)
{
   // Recover the address from the signature
   var signer = new EthereumMessageSigner();
   var recoveredAddress = signer.EncodeUTF8AndEcRecover(message, signature);


   // Compare the recovered address with the expected address
   return string.Equals(recoveredAddress, expectedAddress, StringComparison.OrdinalIgnoreCase);
}

Once the signature is validated, the OnClicked function calls the SignedInCommand. This command is defined by the user through a bindable property and receives an instance of the AuthenticatedUserData class as its command parameter. The AuthenticatedUserData class is custom and can contain any information needed for the specific use case.

//Verifies that the address that signs the SIWE message is the user's address.
var isValid = VerifyPersonalSignSignature(siweMessage.ToString(), signature, address);
if (isValid)
{
   Debug.WriteLine("Success: User authenticated with Ethereum");
   SignedInCommand?.Execute(new AutenticatedUserData
   {
       AuthenticationNonce = siweMessage.Nonce,
       UserAddress = address,
       WalletName = _dappClient.AddressProvider.DefaultSession.Peer.Metadata.Name,
       UserPublicKey = _dappClient.AddressProvider.DefaultSession.Peer.PublicKey
   });
   return;
}


Problems and limitations

Creating an SIWE example with .NET MAUI was challenging due to the limited documentation available for the WalletConnectSharp library. Additionally, while there is a package called WalletConnectSharp.Auth designed specifically for wallet authentication, most wallets were not compatible with this protocol. As a result, we chose to use the personal_sign method, which is widely supported by wallets. However, this method requires the wallet app to be opened twice, which, although a common pattern in web3 applications, might be inconvenient for users who are not familiar with the ecosystem.

The solution also behaves differently depending on the platform. On Android, when a deep link is opened by the Launcher class, the OS displays a dialog allowing the user to choose from the available wallets. However, on iOS, this functionality does not exist, so the OS will open the default wallet app without asking the user which one they prefer.

Blockchain iOS demo image

Future Work

There is a new WalletConnect SDK called AppKit (Overview | WalletConnect Docs), which offers features such as the ability to request wallet authentication with only one interaction. The documentation for this SDK is comprehensive and easy to understand. However, the SDK is not available for .NET MAUI but is available for iOS and Android. Library bindings could be created for MAUI from these platform-specific packages.

Another potential future enhancement is to extend this solution by creating a more complex SiweMessage that improves security. Additionally, the nonce line could be used to link the message to a session and work with JWT tokens. There is an example of SIWE using Blazor that integrates JWT and the Nethereum library, which can be found on GitHub here

Conclusions

We successfully created an example app in .NET MAUI that manages a connection with a wallet app using only C# libraries, without any JavaScript dependencies. This app allows users to sign in with their wallet using the SIWE protocol. All logic related to the protocol and the connection is encapsulated within a single control, making it easy to reuse in other solutions, you can check all the code here

Blockchain is a powerful technology that can enhance software solutions by providing security, transparency, and trust. Although it has recently been overshadowed by the rise of AI, blockchain remains a valuable tool. As current systems are increasingly integrating with AI, blockchain should also be considered for integration, as it can be a powerful differentiator in any business vertical.

At UXDivers, we are committed to delivering exceptional UI/UX across all .NET technologies. Integrating innovative tools like Blockchain is just one way we help our clients stay ahead of the curve, enabling secure, transparent, and future-proof interactions with technology. As we continue exploring cutting-edge technologies, we're excited to push boundaries and deliver solutions that not only meet the needs of today but also unlock the possibilities of tomorrow. Stay tuned as we delve deeper into emerging trends and uncover new ways to bring even more value to your projects! 🚀

Copy link
We’re excited for new projects!

Have a project in mind?
Let’s get to work.

Start a Project