paint-brush
HELP! There’s a JWT in my Metaverse!by@patrickleet
1,667 reads
1,667 reads

HELP! There’s a JWT in my Metaverse!

by Patrick Lee ScottJuly 25th, 2022
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

The advent of Web3 and the spreading usage of wallets will lead to users ditching traditional email/password based account systems, and instead, logging in using their wallet. JWTs, while a bit confusing at first, are incredibly useful for backend engineers, especially those of microservice systems. The “screw-JWT” crowd suggests using simple Session IDs, but this is a step backwards to the early 2000s, when simple layered architectures were the standard, which did not have the complexities of today’s backend systems. Patrick Lee Scott explores using JWTs in a web3 world.
featured image - HELP! There’s a JWT in my Metaverse!
Patrick Lee Scott HackerNoon profile picture

Are JWTs really dead, or are they just misunderstood?

I’ve seen quite a few articles lately suggesting that the advent of Web3 and the spreading usage of wallets will lead to users ditching traditional email/password-based account systems, and instead, logging in using their wallet.


To be honest, after using a few dApps, the super simple workflow of a click or two of your wallet that pops up is indeed a superior experience.


Many of these articles go on to say “yay we don’t need JWTs anymore!”.


This is where I disagree.


JWTs, while a bit confusing at first, are incredibly useful for backend engineers, especially those of microservice systems. Especially, considering that these systems - a great number of them - already exist and already integrate with JWTs! Ethereum is great and all, but we really do not need to reinvent the wheel. It’s good to be able to continue using the same backend tools you are used to when you need to.


Logging in with MetaMask proves it’s you - but how do you prove to future API calls it is you?


The “screw-JWT” crowd suggests using simple Session IDs, but this is a step backward to the early 2000s, mind you, when simple layered architectures were the standard, which did not have the complexities of today’s backend systems.


Unfortunately, Session IDs can not be verified without an additional round trip to the database to determine whether or not the Session ID granted belongs to a valid session. This means when the backend service receives the request that contains the session ID, it then makes a request itself to the auth server asking “is this active” - and then every other service in the Microservice system asks the same.


If there are several services involved, that could be several additional round trips to the auth service.


In order to remedy this situation, cryptographic experts got thinking.


What they came up with was JWTs - now part of the OpenID standard, you know, the one that Keycloak, Auth0, and others help you to implement.

JWTs, y’all

The solution was to grant a set of tokens - JSON Web Tokens to be precise. That set consists of an AccessToken, a RefreshToken, and an IdToken. These tokens were then “signed” by a secret - usually called the ClientSecret. Signing, just so we’re on the same page, is a cryptographic hashing algorithm, in the case of JWTs – usually HS256 (the default for Auth0). In the case of HS256 the ClientSecret is used as the input and hence, becomes the key needed to successfully decipher that hash - or “verify” it.  With RS256 and ES256 a public/private key pair are used, i.e. signed by private key, and verified with public key on the client.


This means if a backend service receives one of these tokens, and it knows the ClientSecret, it can verify that the token was actually issued by the auth service which signed that token. The token used when attempting to access a backend service is the AccessToken. These tokens specifically contain information about who the user is as well as their “claims”, or in other words, what they are allowed to do formatted for the system that cares about it.


For microservice systems, this means each service that cares about verifying identity just needs to know about the ClientSecret as they can verify the JWT is authentic using it, rather than a roundtrip to the database. In a system with many microservices, this can reduce many additional round trips to the database, making the entire system more scalable.


Access tokens, if stolen, can be used maliciously, and for that reason, it’s important to take the appropriate precautions when designing a system to use them.


The minimum set of precautions, on top of signing and verifying the tokens consist of:


  1. Setting an expiration date on the Access Token JWT of 5-15 minutes and making sure tokens are not expired when received

  2. Don’t store the Access Token except for in memory

  3. Issue Refresh Tokens that can be stored and sent to a server for verification, and refreshed along with using the ClientSecret and ClientId that issued it.

  4. Only use when transmitted over TLS connections (HTTPS)

  5. Use HTTP Only cookies so they can not be modified on the browser side

  6. CORS settings

  7. A key of the same size as the hash output (for instance, 256 bits for "HS256") or larger MUST be used with this algorithm. - RFC7518, note: Auth0 uses 512 bits for HS256.


A confidential client needs an intermediary server, such as Node.js - this server is a proxy to the auth service so the client need not know about the client secret. A public client exposes the client secret, and there isn’t a proxy server between the browser and the auth service. This can be restricted further with CORS settings so only requests from certain domains are allowed.


Additional precautions include:


  1. Use Rotating Refresh Tokens – each one can only be used once. When used it is exchanged for a whole new set of tokens, and the exchanged refresh token is revoked. If this strategy is employed it’s worth noting that users with a poor internet connection can sometimes not receive the response to the refresh request and try again. For this reason, a short grace period of 30 seconds or so will allow for them to hit refresh and try again.
  2. Allowing user to revoke all of their existing refresh tokens by logging out.


And probably more - for example, if you detect someone attempting to use a revoked refresh token, you could revoke all of that user’s active refresh tokens.


All of these precautions, admittedly, can make getting it right a bit tough. There’s a lot to understand. There’s also usability concerns. A user becoming logged out of your site every 5 minutes would feel pretty annoying to them. To avoid this, a silent refresh loop must be implemented in the consuming application to continually refresh the token set.


That said, on the other side of getting it right, is being able to securely integrate with all of your backend systems in a scalable way, as well as many existing tools - such as Hasura, which can autogenerate all your APIs for you based on a connected Postgres’ DB schema. Thus, being able to integrate easily with existing tooling can save a lot of development time.


If you use OpenId already, it’s likely you already have these things in place. It is, after all, an authentication standard.


So how can we keep the convenience of using JWTs in a Web3 and login with MetaMask world?

JWTs and Web3?

Let’s start by understanding the OpenId authentication flow used for SSO.


  1. You visit a website and want to login with your account - you click the login button
  2. You are redirected to the SSO login page - you login with an email/username and password
  3. You are redirected back to the original application with a set of JWTs which are stored appropriately and used in silent refresh background flows.


In the web3auth world, we are replacing step two with using your wallet’s private and public key pairs to sign a challenge. The redirect is not needed or desired.


  1. You visit a website and want to login with your account - you click the login button

  2. You are receive a challenge from the auth server which pops open your wallet asking you to “Sign” the challenge. Press sign.

  3. The auth server verifies your signature and issues you a set of JWTs which are stored appropriately and used in silent refresh background flows.



We’re simply replacing the SSO style redirect flow with a challenge that is signed by your wallet. The flow after receiving the tokens remains the same as OpenID. That means you could for example, switch from using OpenID to using web3auth with a JWT issuing server, and nothing about using those tokens after they are granted would need to change. All your existing backend integrations with tools like Hasura remain exactly the same.


This is exactly what I want as a Full Cycle developer. I don’t want to reinvent the wheel. I want to replace OpenID with web3auth and still be able to use all of the powerful tools that I am used to.


Unfortunately, I couldn’t find a web3auth server that did this as well as the security precautions. I found a few projects demonstrating techniques in the process, but not the whole flow end to end.


So I got to building…


I built this here auth server: https://github.com/CloudNativeEntrepreneur/web3auth-service


And this here SvelteKit integration to go with it, that implements all the things - silent refresh, yada yada - all those things I mentioned above: https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth


Of course if there was gonna be a GraphQL client and example, then there needed to be a GraphQL server and database as well, so I also provided examples of that: https://github.com/CloudNativeEntrepreneur/example-hasura + https://github.com/CloudNativeEntrepreneur/example-readmodel


This example uses the Zalando Postgres operator and SchemaHero, so all you need to do is declare your DBs and describe your schema in YAML, and Hasura will autogenerate all the GraphQL APIs you need. AND, I made the auth server with Hasura in mind, so it has the proper claims to integrate with Hasura's RBAC and permissions, which are quite robust.


And of course, you need a place to run all that, and so, a local dev cluster that sets up all the tooling like istio, and the operators, and SchemaHero for you! https://github.com/CloudNativeEntrepreneur/local-dev-cluster


But who knows how to even use all that?


So that's why I made this meta repo: https://github.com/CloudNativeEntrepreneur/web3auth-meta


Using that meta repo will clone all the projects you need down into the right places, and run them all together.


Finally, to run all the projects together, you need tools installed, and installing tools is annoying - so I made this repo here that will install them all for you! https://github.com/CloudNativeEntrepreneur/onboard


I also published sveltekit-web3auth on npm, and made a template out of a SvelteKit project that uses it and has GraphQL set up and integrated with authentication to a Hasura instance so when you're ready to make your own projects, you can use that as a template! https://github.com/CloudNativeEntrepreneur/sveltekit-web3auth-template


If you're not ready for the web3 auth world yet, you can also use https://github.com/CloudNativeEntrepreneur/sveltekit-oidc, which comes preconfigured to connect to your local dev cluster, and a Keycloak instance that's set up within it. Seeing as how both projects issue JWTs, the goal is for the auth system to be interchangeable - use web3auth or classic OIDC - upstream usage of the tokens are the same.


Now go and make some hybrid dApps with autogenerated GraphQL APIs and robust RBAC and permissions and subscriptions and authenticated SSR pages and silent refresh loops and rotating refresh tokens and stuff!

Conclusion

In conclusion, no, JWTs and login with Ethereum/metamask are not mutually exclusive. In fact, if you like developer productivity and integrating with existing tools, I think you’ll do quite well using JWTs AND web3auth.


Cheers!


I’m available for consulting! If you’re interested in my help on a project you’re working on, send me a message on Twitter!