DEV Community

Frank Szendzielarz
Frank Szendzielarz

Posted on

Bringing Passkeys to Enterprise and Gaming apps with Stellar and .NET

NOTE: A prerequisite to using or understanding the supplied code is that the Soroban/Stellar environment is set up. It's not much work, and the guide is here

Interested in Passkeys?

Authentication and authorisation seem to be the same problem for all developers, no matter the environment. Game developers dealing with game users have to handle sign up and sign in, just as enterprise app developers have to handle the same for company staff. Then there is the question of if different apps from the same company or for the same company should share the same users, and if so how are authorisations carried from one application to another. There are many approaches to these kinds of challenges, and the ways forward today involve Passkeys.

If you work in a sector such as healthcare or banking then you have already heard of Passkeys. If you are a general web developer distanced from authentication tasks you might not have yet.

What this article talks about is how to add Passkey functionality to your apps. Particularly if you are doing Unity game dev development or commercial systems development both requiring .NET. In addition though this can come with the added benefit of account management using something called "Stellar".

This gives us two topics to follow up on. "What are Passkeys?" and "What is Stellar?"

Before we move forward though, there is one interesting question I'd like to ask, and I wonder what your thoughts are. If you have dealt with authentication/authorisation tasks, have you noticed that systems tend to outsource that part of the logic to trusted third parties a lot, or not? In my recollection, to in-house identity or not used to be a contentious issue. These days it seems to me that external identity providers are often the norm.

What are Passkeys?

The defacto norm, though it is waning, is for registrations to involve usernames and passwords, or inhouse credentials such as Windows login, and so on.

Naturally therefore the de facto norm is for servers to be compromised by hackers, who steal passwords, and for users to be lured through phishing attacks to deceptive clone sites to lose their identity, private details, money and sometimes more.

Passkeys are a technology that the various big players have been working on for quite some time to try and address these kind of problems. It's an authentication technology, and the protocol for the web part of it is called WebAuthN.

In a nutshell, with passkeys, when you sign up and register at some website then your device (a mobile phone, your local OS, a security key) registers a new cryptographic keypair and let's the server know what the public key is. From then on, that device can sign a random array of bytes called a 'challenge', and only for that website domain, and prove that you are who you are. The protocol makes sure that everything is fine.

There aren't any usernames, or not necessarily, no passwords, nothing to steal, and the passkeys work only on that website, no other, so nothing to Phish. This is why from a security perspective they are so appealing, and why many of you now are probably being mandated (in the corporate world) to re-wire your systems to support them. In the consumer world, we see a growing trend of passkeys replacing prior 2FA methods, with SMS auth going the way of the dodo, and as the technology matures passkeys slowly becoming the new norm.

What is Stellar?

Stellar is essentially a public cloud that belongs to the masses, differing from cloud providers like Azure or AWS in the sense that it is distributed, robust, and independent of where the nodes in its network run. The key thing for auth is that it is super cheap, super fast, super reliable, and it allows accounts to be totally programmable, while allowing those same accounts to be transferable between apps, companies, and through the lifetime of the user.

Private subclouds can be created, for example. Banks, governments, indie game studios, and back bedroom developers can see it as public infrastructure for backend services.

What Stellar offers, in relation to Passkeys, is the ability to offload account management to that public infrastructure, and potentially avoiding cloud provider costs. In simple terms, you can treat Stellar as just a black box and send it tasks, such as "Create Account", "Sign in to account", "Add something to this account" and so on, in a technically simple way.

The key difference though, compared to inhouse account management or outsourcing to classic cloud providers, is the economic model. There are only small fees for account related data, and transaction fees related to account usage, but those fees are in return for security, reliability, transparency, immutability, and various other characteristics that can sometimes be essential for your use case.

For example, if you were developing for the public sector where anonymised records needed to be indelibly, immutably recorded for potential retroactive inspection, Stellar works. If you wanted to create user accounts that might need transferring not just from app to app but from company to company, Stellar works, as on Stellar those account can be made permanent and independent of how they were created.

Tying Passkeys and Stellar Together

In this article I go through a demo app. It is written using .NET and C#, so if you are thinking about adding Passkeys or Stellar to your Unity game, or looking to address auth scenarios in the enterprise, this should be familiar to you.

The demo app is running here

The source code is available here

Before going into details, we'll run through the Sign Up scenario visually.

Sign Up

One thing you will notice from the sign up screen is there is no mandatory request for personal data:

Image description

An optional display name, that the server can save.

When we press Register User...

Image description

...the operating system asks us where we want to save our new passkey. Velvet is the name of my android phone. I can save it to that, or if I select a different device, then I can pair it using a QR code. It is as simple as that.

My phone gives me this message:

Image description

and then I get this in the desktop app:

Image description

Once the passkey is saved, then when we click Sign In we can use that passkey to quickly login.

The flow is absolutely simple, intuitive, and these days with the latest OS's without friction at all, so it is clear to see that a UX like this could well become the future.

Account Management

So, the user registered a passkey. The server records the id, or the public key, and this can be used for authentication in the sign in step. Nice, safe, but something is missing.

Signing up, in most applications, should mean the creation of an account. Signing in should mean authentication through the signing of random challenges, to prove the user is who they are, and potentially to authorise specific operations on those accounts.

This is where the complexity start to grow.

What if we are developing an indie game, and our user base is growing, and then we want those users to have the same credentials when we release the second indie game?

What if we want to be able to add special functionality to these accounts, such as allowing users to be able to have an inventory of assets?

What if we want the user to be able to associate their account with anonymised health data and carry that out of the system?

What if we want to be able to link accounts together in some way?

In our demo, we tackled a simplified version of these problems using Stellar behind the scenes. Using the Stellar .NET SDK, when the Passkey registration happened, a Stellar Smart Account was created for the id of that particular passkey.

That Smart Account can be implemented in any way you like, as it is essentially a program that runs securely on the Stellar network. When users authorise actions with their Passkeys, those Smart Accounts can act as gatekeepers deciding what should happen. Further, other users may be permitted, if you wish, to transfer things between these Smart Accounts. In this way, for example, a game studio could allow asset transfers between game accounts irrespective of the game! A company employee might sign contracts by signing a simple "I have read X" operation. And so on.

The Nitty Gritty

This is where we start getting into the details of both WebAuthN (the web version of the passkey protocol) and Stellar. Before I do though, I want to make sure a huge shout out goes to this superstar here who breathed life into Stellar passkey smart contracts.

The place to start is the JS code for handling the user registration, on the page served by the particular domain.

What we see is that when the register button click event is handled on this line then in our case we get a simple call to the server to register the credential. Important to note is that at this point, even on the JS client side, we can use the JS Stellar SDK to create an account without the server's involvement. For simplicity's sake, our example uses the .NET SDK on the server side to perform that task.

After the browser is asked to create a new passkey here:

Image description

the server is invoked here to save the passkey id or public key:

Image description

but our server goes off and does something more a little more sophisticated:

Image description

So what is actually going on?

At this point in the code the web route handler is deploying a Stellar Smart Account for the specific passkey id.

The actual documentation and explanation is in the code, and really it's just about understanding the .NET SDK for Stellar. The actual real nuts and bolts are in the Stellar Smart Contracts themselves.

If we open the project in Visual Studio 2022 this is what we see:

Image description

Notice that we have a Contracts folder, and despite the C# and JS default languages we have some Rust there!

Image description

This is for the simple reason that Stellar accounts and Stellar deployed "smart contracts" are written in Rust and compiled to WASM.

Once the C# deploy contract code is called, the SDK communicates with the Stellar network telling the factory account (which is already deployed) to deploy yet another account instance, called the 'webauthn-secp256r1', but customised to contain the public key of the newly registered user's passkey.

This is later used to authorise other accounts, which are completely programmable, based on passkey signatures. If the user should wish to execute some operation on some other, new, programmable, Stellar account, and if that account requires authorisation on the participating account, then that user will have to pass in a server passkey signature. Stellar will make sure that nothing can happen without it.

Building and Deploying RUST?

Don't worry. The demo project has it all taken care of.

Simply modify the Build Configuration here

Image description

and if we examine the build targets in the project file, we see the behaviour:

Image description

For this kind of build time deployment of Stellar accounts, we need to also set environment variables. Once the Contracts are built, then we need to shut down Visual Studio and run this environment variable tool:

Image description

However, at runtime, and at different deployment sites, we can use launchsettings.json:

Image description

Understanding the Smart Account

So to recap, this is what has happened so far:

  • Our user has signed up
  • Behind the scenes a passkey was created on their device and its id stored on the server
  • A Smart Account was deployed on the Stellar network for that particular passkey

One might ask then, so what? What good is it?

The utility comes from subsequent accounts, or 'contracts,' that can be made to depend on it. First though, let us quickly dissect the anatomy of the authorising smart account. Open the file here:

Image description

All of the important functionality is contained here:


fn __check_auth(
     env: Env,
     signature_payload: Hash<32>,
     signature: Signature,
     _auth_contexts: Vec<Context>,
 ) -> Result<(), Error> {
     // Verify that the public key produced the signature.
     let pk = env
         .storage()
         .instance()
         .get(&STORAGE_KEY_PK)
         .ok_or(Error::NotInited)?;
     let mut payload = Bytes::new(&env);
payload.append(&signature.authenticator_data);
     payload.extend_from_array(&env.crypto().sha256(&signature.client_data_json).to_array());
     let payload = env.crypto().sha256(&payload);

     env.crypto()
         .secp256r1_verify(&pk, &payload, &signature.signature);

     // Parse the client data JSON, extracting the base64 url encoded
     // challenge.
     let client_data_json = signature.client_data_json.to_buffer::<1024>();
     let client_data_json = client_data_json.as_slice();
     let (client_data, _): (ClientDataJson, _) =
         serde_json_core::de::from_slice(client_data_json).map_err(|_| Error::JsonParseError)?;

     // Build what the base64 url challenge is expected.
     let mut expected_challenge = *b"___________________________________________";
     base64_url::encode(&mut expected_challenge, &signature_payload.to_array());

     // Check that the challenge inside the client data JSON that was signed
     // is identical to the expected challenge.
     if client_data.challenge.as_bytes() != expected_challenge {
         return Err(Error::ClientDataJsonChallengeIncorrect);
     }

     Self::extend_ttl(env);

     Ok(())
 }
Enter fullscreen mode Exit fullscreen mode

Cutting a long story short, this takes in the Passkey signed payload during a Signin, as long as a call to some other account :

  • Requires that the caller of the function on the other account be authorised
  • The call has the passkey signing data in its authorisation payload

We can see how that happens, in the C#, quite easily, because in our demo app we have a simple account called contract-sigin-record. The purpose of that account, or contract in other parlance, is to just log signins. It represents a demo use case, namely, just log whenever someone signs in, but do it permanently on a public record:

var simTxnBuilder = new TransactionBuilder(ownerAccountData);
simTxnBuilder.SetFee(0);

simTxnBuilder.AddOperation(
    new InvokeContractOperation(
        new SCContractId(signinContractId),
        new StellarDotnetSdk.Soroban.SCSymbol("log_sign_in"),
        [
            StellarDotnetSdk.Soroban.SCAddress.FromXdr(smartAccountXdrSCAddress)            //the address of the smart account signing in
        ]
    )
);
Enter fullscreen mode Exit fullscreen mode

Note that above the transaction is trying to call the signinContractId on a function called "log_sign_in" but with an argument of the callers smart account.'

If we look at the signin contract itself, the log_sign_in function shows this:

Image description

Notice that the argument passed in, the id of our user's Smart Account, is invoked with

source.require_auth()
Enter fullscreen mode Exit fullscreen mode

This means "ask the smart account to authorise things". But, naturally, the question is "how?" and "with what?" The answer is that the call to the log_sign_in is made with additional arguments, which we find here

In short, the code gets the webauthN passkey payload and sends it as part of the call. When the target account asks for authorisation, the __check_auth is invoked on the smart account indirectly via require_auth.

This is basically it. With this mechanism, Stellar accounts can contain and do anything, and be used with passkeys.

Summary

In summary, I'd like you to feel there are a number of takeways:

  • If you are interested, there is a demo project and source that you can play with. It's on github here and it's deployed here
  • Passkeys are easy, the future, the now, and the way users should be authenticating
  • Passkeys are only part of any identity and auth solution, and account management is always another side of the coin
  • With Stellar, you can easily make accounts and tie them to passkeys using .NET, and these accounts can be fully offloaded and made independent from your infrastructure, without investment in security, backups, etc

There is so much more that this article could go into on this topic and on the details of the demo source, but it would be best left to follow-up articles. Consider it an intro, and please feel free to contact me if you would like to learn more.

Top comments (0)