There are many guidelines on using Ethereum's accounts for signing in to an application. This enables a Sign In With Ethereum experience, that relies on the user signing an attestation of their identity with their private key, which is used to log them in to an app. This has the main benefit of decentralization over the traditional Sign In With Google, since we no longer depend on Google as a centralized identity provider.
However, some time ago, we wondered what would happen if we flipped this around. In other words, instead of using Ethereum as a replacement to Google to sign in to an app, what if we could use Google to sign in to Ethereum? As a concrete use case, could we support recovery of an Ethereum identity contract using Google Sign In, without adding any extra trusted components? Turns out we could.
Identity contracts & recovery
Identity contracts, also known as smart wallets, are a pattern for moving the user's identity from their local keys to the blockchain itself. For all practical purposes, the user identity is no longer an externally owned, but a smart contract itself. This allows a user to have multiple devices, each of them with its own private key, which are registered as operators of a single identity contract. This contract's address then holds the users' funds and represents them online, and is used to interact with any other Ethereum protocols. Authereum, Gnosis Safe, Universal Login, and Argent (to cite a few examples) implement this pattern.
Having access from multiple devices ensures that, if you lose one, you can still access your online identity from a different one. However, if you lose access to all of them, you still need some recovery option baked-in, since forgot my password does not work in a decentralized world. One of the most popular options to solve this is problem is social recovery, where you designate a set of friends to grant you access back to your identity should you get locked out.
With this scenario in mind, together with @frangio, we set out to build an experiment: given that most users already trust Google with their digital life, could we add a Google Recovery option, in which the user's identity in Google could be used to regain access to their identity contract?
How it looks like for a user
We built a small client-side-only demo app to showcase how this experience looks for the user. The user first signs in with Google to a page, in order to get their Google user ID. This ID is stored when deploying their identity contract, as well as the identifier of the application the user signed in to.
Note: This application (which will be referred to as the audience later) needs to be created once from the Google Developer Console, specifying the domain where it'll be hosted.
Afterwards, if the user switches to a different Ethereum account, they can go through a recover flow where they are asked to sign in with Google again and send a transaction to the identity contract from their new Ethereum account.
The authentication result is then sent to the identity contract, which recognizes the user ID validated by Google, and adds the new Ethereum account as owner of the identity contract. This allows a user to regain access to their decentralized Ethereum identity with just a single transaction - and a Google Sign In.
How it works under the hood
We are using Google Sign In for Websites, which is a flow designed to work without a server. In this flow, the user is prompted to sign in to their Google account, and Google returns a signed JSON Web Token (JWT) with the user's identity.
JWTs are composed of three sections, all base64-encoded: a header, a payload, and a signature. The header contains information such as the signer and the audience, while the payload contains the user information. The interesting thing here is that the JWT is compact enough that it can be decoded and verified in a smart contract.
In our project, all this magic happens in the recover
method of the sample Identity contract we have built.
function recover(string memory headerJson, string memory payloadJson, bytes memory signature) public {
string memory headerBase64 = headerJson.encode();
string memory payloadBase64 = payloadJson.encode();
StringUtils.slice[] memory slices = new StringUtils.slice[](2);
slices[0] = headerBase64.toSlice();
slices[1] = payloadBase64.toSlice();
string memory message = ".".toSlice().join(slices);
string memory kid = parseHeader(headerJson);
bytes memory exponent = getRsaExponent(kid);
bytes memory modulus = getRsaModulus(kid);
require(message.pkcs1Sha256VerifyStr(signature, exponent, modulus) == 0, "RSA signature check failed");
(string memory aud, string memory nonce, string memory sub) = parseToken(payloadJson);
require(aud.strCompare(audience) == 0 || true, "Audience does not match");
require(sub.strCompare(subject) == 0, "Subject does not match");
string memory senderBase64 = string(abi.encodePacked(msg.sender)).encode();
require(senderBase64.strCompare(nonce) == 0, "Sender does not match nonce");
if (!accounts[msg.sender]) {
accounts[msg.sender] = true;
accountsList.push(msg.sender);
}
}
This method verifies Google's signature to check the JWT is valid. It then checks that the audience
(ie the app that requested the token) is the same as used when creating the identity contract, and then checks that the subject
(ie the user) is the same as well. This requires base64 encoding and RSA verification, all done on-chain. As you can imagine, this process consumes a lot of gas; but recovery should not be a frequent operation. If everything is verified correctly, then the user's new Ethereum address is added to the identity contract.
However, we now have a missing piece: how do we prevent someone from front-running the transaction, grab the JWT sent to the contract, and use it to gain access themselves?
The solution we found here is tricking Google into signing the new Ethereum address as part of the JWT itself. And we are doing it by setting the Ethereum address as the JWT's request nonce, which in turn gets signed as part of the JWT itself, which we can then validate in the identity contract. A bit hacky, but works!
Note: As you may have seen if you tried the demo, the nonce is never exposed to the user as they Sign In With Google in the application, meaning that they could be inadvertently getting Google to sign a different Ethereum account if the app is malicious. This could even be done by any other application in which the user signs in with Google, which uses the JWT behind the scenes to gain access to the user's Ethereum Identity. We currently mitigate this by validating the audience (ie the Google app identifier) in the smart contract, so only the original app can recover access to the contract. This requires the user to trust this single app, which could be further mitigated by storing the page in IPFS to ensure its immutability after it was audited once.
However, this still requires trust on the owner of the Google Developer project, who may add new malicious domains and trick the users into signing in there, or even close the project removing the possibility of recovery. That in turn could be mitigated by having users could download the page and run it locally, configuring their own Google Developer project, though the user experience is far from ideal.
A keys oracle
The last piece of the puzzle is how to get the public keys used by Google to sign the JWTs, since these are rotated frequently. These keys are advertised in this URL, as indicated on Google's OpenID configuration.
At the moment, we are storing these keys in a trusted JWKS (JSON Web Key Set) contract which keeps track of these keys. However, the owner of the contract (currently us) could inject a malicious public key, and use it to self sign fake JWTs, gaining access to all users' identity contracts. This makes this a contract a key piece of the infrastructure.
An oracle solution could be used to fill in this gap, such as Chainlink or Provable. In particular, since the keys are advertised from a single URL endpoint, an integration with TLSNotary could be used here.
FAQ
-
Can I try this out?
Sure! Head over to https://identity-recovery-demo.openzeppelin.com/ on the Rinkeby network to test our demo. Note that the keys may have already been rotated, and updating them is a manual process - so if recovery fails, let us know! -
Show me the code!
Ok, not an actual question, but we get the point. Everything's available on our github! -
Can I use this in production?
Dear lord, no! There are many things to figure out still (such as trust on the google developer project, or the oracle for the signing keys), and the whole solution relies on the hack of using an ethereum account as a nonce - which is even undocumented -
Why did you do this?
Because we can! We were interested in seeing how far we could push Ethereum -
Is that a good enough reason?
Sure! We are buidlers after all, right?