Bringing decentralized identity to traditional apps
TL;DR: In this technical post for developers, I walk through how we added Blockstack ID support to the Discourse forum software.
At Blockstack, we're building a new internet for decentralized apps. A new internet implies new apps. But what if you've already built a current app?
You can still give your users ownership over their identity by integrating Blockstack Authentication in your centralized, client-server app.
This is exactly what we did with the Blockstack Forum. We really liked the Discourse forum software and wanted to give users the option to sign in with their Blockstack IDs.
Enabling Blockstack Authentication on a centralized app potentially enables a range of functionality:
- single sign on - users with a Blockstack ID don't have to register again or sign up for your app
- an instant profile system - users can bring their existing profile to your app
- client-side encryption - you can encrypt user data on your users' client devices before sending it to your server. This is great for user privacy and may offer legal and security advantages for your business. Both hackers and law enforcement aren't able to access user data that has been encrypted with keys generated from a Blockstack ID. Hopefully this means they leave you alone!
In this blog post, I'm going to walk through how we added Blockstack Authentication to our Discourse forum. I'll walk through the libraries I created and the ones I modified. If you just want to enable Blockstack Authentication in your Discourse instance, you can use the Blockstack Discourse plugin that was the result of the work described in this blog post.
By explaining the process I went through to enable decentralized authentication on Discourse, I hope to make it easier for you, the reader, to do the same for your favorite platform or development stack!
Where we're going
Before we get started, let's look at where we're going. Blockstack Authentication works like most other authentication systems in that it gives you a unique identifier for an authenticated user. You'll store this identifier as a unique key on your app's User
model and use it to look up the user.
If you're not already familiar with Blockstack Authentication, I'll review the high-level components in this post. For a more detailed look, take a look at my post on how the Blockstack Token Sale App worked.
Authentication Tokens
The first thing to understand is that Blockstack Authentication makes heavy use of JSON Web Tokens (JWTs). A JWT is created by your app when it wants to authenticate and the user generates another JWT which is used to prove their identity to your app.
JWTs are JavaScript objects that are signed by a private key. The JWT standard specifies a couple different standard signature types. Unfortunately, the standard doesn't include ECDSA using secp256k1, the elliptic curve used by Bitcoin and most other cryptocurrencies, which is the the elliptic urve we use in Blockstack. This means we'll have to add support for this curve to a JWT library if the JWT library for our language doesn't already include it.
Adding support for secp256k1
Discourse is written in ruby, so I had to add support for secp256k1 to the jwt-ruby
library. To do this, I modified the library so that when it is presented with a JWT signed by an algorithm name ES256K
that it uses the appropriate routines to verify the signature. You'll also want to add support for signing JWT using this signature type as well.
As of the writing time of this blog, we have libraries that that support this signature type in both JavaScript and Ruby.
Verifying Blockstack authentication responses
Discourse is a traditional client-server app where code running on the server is trusted by both the app owner and users using the app. In such a client-server app, it is ultimately the server that decides who a user is and vouches for that user's identity to other users of the app.
Code running on the client can never be trusted by other users or the app developer because the client is in a position to change the code.
Because of this, in a traditional client-server app we want to verify Blockstack authentication responses on the server.
To do this, we need to write code that performs the function of the verifyAuthResponse
function in blockstack.js.
This function decodes the following:
- Decodes the JWT token and extracts the payload
- Verifies the user's public key in the payload matches the issuer, which is the user's identity address.
- Verifies that the signature on the JWT matches the public keys
- Verifies that username that the user claims is owned by the identity address corresponding to the issuer by performing a lookup against the Blockstack Core node trusted by the app
- Verifies the expiration date and issuance dates are valid
In our Blockstack ruby library, we created a method verify_auth_response
that performs these functions.
Using the payload
Once you've verified that the authentication response token is valid, you can decode the payload and get to work. If you're only looking to verify that a certain user is who they say they are, you can read the iss
value and use that as the unique key for the user in your database. You can also go beyond that and use other information in the payload, such as the username
claimed by the user.
In our Discourse implementation, our next step is to integrate our blockstack-ruby's verify_auth_response
method and some client-side code from blockstack.js into a Blockstack-specific strategy for the Omniauth authentication system used by Discourse.
Omniauth is a piece of middleware widely used in the Ruby on Rails ecosystem which means that our work adding support to this framework for Discourse can be easily reused by any app that uses Omniauth.
Omniauth defines two entry points into the strategy. The first is the request phase. This route gets loaded when a user starts to authenticate using the strategy. The second is the callback phase. The callback phase route gets loaded upon redirect after the user successfully approves or denies the authentication request.
Request phase
In our Omnifocus strategy's request phase, we want to generate a Blockstack authentication request token. Since Discourse is a web app, the easiest way for us to do this is by loading Blockstack's canonical library, blockstack.js, into the request phase webpage, and using the redirectToSignIn()
method to initiate the authentication request in JavaScript the same as a truly decentralized app would do.
All Blockstack apps require a manifest file that includes information such as the name of the app and its icon. We're able to modify the callback request path so that when ?manifest=true
is appended to the path, it serves a valid manifest file generated from Omniauth configuration settings instead of running the callback request logic. This path is included in the authentication request.
Callback phase
After the user approves authentication in their authenticator app (typically the Blockstack Browser), they are redirected back to the path defined by the callback_phase
of the Blockstack Omniauth strategy.
The authenticator appends the authentication response token to the callback phase URL in the authResponse
query parameter. Our strategy reads this parameter and uses blockstack-ruby
s verify_auth_response
method to verify and decode the token.
Going forward, we take the decoded tokens payload and load the various pieces of information into the pre-defined fields provided by Omniauth.
Integrating this into Discourse
Now that we've created the parts that we'll need to get decentralized authentication working in our Discourse instance, it's time to put them together in a Discourse plugin.
Our Discourse plugin configures the Blockstack Omniauth strategy and adds it as middleware to our Discourse instance.
After a user is authenticated by the middleware, the plugin looks for an existing user in the database with decentralized id of the authenticated Blockstack user and signs them in. If a user doesn't exist, it creates a new user and pre-populates some of the User
fields with existing Blockstack ID profile information.
Future steps
Blockstack Authentication provides an app-specific private key that we can use to encrypt data only for the user. In our Discourse plugin implementation, we don't do anything with key.
The app private key is encrypted in the authentication response token with a randomly generate private key, the transit private key, that is generated and stored in the users' browser at the start of authentication.
This means that the server never has access to the app private key even though we send the authentication response token to the server for verification because it doesn't have the key to decrypt it.
Imagine we wanted to extend Discourse so that users could make posts or send messages encrypted for certain users in the community. We could extend our Discourse plugin so that it decrypts the app private key in the user's browser and then uses that to encrypt posts and messages before they're even sent to the server. This would allow users to have discussions and share information without the forum operating being privy to the actual content.
Another idea would be to use the app private key to create a cryptocurrency wallet for the user. This might be a way to enable tipping for all users in your community.
How can decentralized authentication change your app? Leave your comments or questions in this forum thread.