This presents a method to replace the usual signup/login design pattern with a minimal ethereum native scheme, that doesn’t require passwords, backing up private keys nor typing seed phrases. From the user's point of view it will be very similar to patterns they’re already used to with second factor authentication (without relying in a central server), but for dapp developers it requires a new way to think about ethereum transactions.
The unique identifier of the user is a contract that implements both Identity and the Executable Signed Messages ERCs. The user should not need to provide this address directly, only a ENS name pointing to it. These types of contracts are indirectly controlled by private keys that can sign messages indicating intents, which are then deployed to the contract by a third party (or a decentralized network of deployers).
In this context, therefore, a device "logging into" an app using an identity, means that the device will generate a private key locally and then request an authorization to add that key as one of the signers of that identity, with a given set of permissions. Since that private key is only used for signing messages, it is not required to hold ether, tokens or assets, and if lost, it can be simply be replaced by a new one – the user's funds are kept on the identity contract.
In this context, ethereum accounts are used in a manner more similar to auth tokens, rather than unique keys.
The login process is as follows:
The first step of the process is to request from the user the ENS name that points to their identity. If the user doesn’t have a login set up, the app should–if they have an integrated identity manager–provide an option to provide a subdomain or a name they own.
UX Note: there are many ways to provide this interface, the app can ask if they want to signup/login before hand or simply directly ask them to type the name. Note that since it’s trivial to verify if a username exists, your app should adapt to it gracefully and not require the user to type their name twice. If they ask to signup and provide a name that exists then ask them if they want to login using that name, or similarly if they ask to connect to an existing name but type a non-existent name show them a nice alert and ask them if they want to create that name now. Don’t force them to type the same name twice in two different fields.
If the user doesn’t have an identity, the app should provide the option to create one for them. Each app must have one or more domains they control which they can create immediate subdomains on demand. The app therefore will make these actions on the background:
All those steps can be designed to be set up in a single ethereum transaction. Since this step is not free, the app reserves the right to charge for registering users, or require the user to be verified in a sybil resistant manner of the app’s choosing (captcha, device ID registration, proof of work, etc)
The user shouldn’t be forced to wait for transaction confirmation times. Instead, have an indicator somewhere on the app that shows the progress and then allow the user to interact with your app normally. It’s unlikely that they’ll need the identity in the first few minutes and if something goes wrong (username gets registered at the same time), you can then ask the user for an action.
Implementation note: in order to save gas, some of these steps can be done in advance. The app can automatically deploy a small number of contracts when the gas price is low, and set up all their main variables to be 0xFFFFFF...FFFFF. These should be considered ‘vacant’ and when the user registers one, they will get a gas discount for freeing up space on the chain. This has the added benefit of allowing the user a choice in contract address/icon.
If the user wants to connect with an existing identity, then the first thing the app needs to understand is what level of privilege it’s going to ask for:
Manager the higher level, allows the key to initiate or sign transactions that change the identity itself, like adding or removing keys. An app should only require this level if it integrates an identity manager. Depending on how the identity is set up, it might require signature from more keys before these transactions can be deployed.
Action this level allows the key to initiate or sign transactions on address other than itself. It can move funds, ether, assets etc. An app should only require this level of privilege if it’s a general purpose wallet or browser for sending ethereum transactions. Depending on how the identity is set up, it might require signature from more keys before these transactions can be deployed.
Encryption the lower level has no right to initiate any transactions, but it can be used to represent the user in specific instances or off-chain signed messages. It’s the ideal level of privilege for games, chat or social media apps, as they can be used to sign moves, send messages, etc. If a game requires actual funds (say, to start a game with funds in stake) then it should still use the encryption level, and then require the main wallet/browser of the user to sign messages using the ethereum URI standard.
Once the desired level is known, the app must take these steps:
addKey(PUBLICKEY,LEVEL)
.Here's an example of a EIP681 compatible address to add a public key generated locally in the app:
ethereum:bob.example.eth?function=addKey(address='0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',uint=1)
If adding the new key requires multiple signatures, or if the app receiving that request exclusiveky deals with executable signed messages and has no ether on itself, then it should follow the steps in the next section on how to request transactions.
As before, the user shouldn’t be forced to wait for transaction confirmation times. Instead, have an indicator somewhere on the app the shows the progress and then allow the user to interact with your app normally.
After step 2, the end result should be that your app should have the identity address of the user, their main ens name and a private key, whose public account is listed on the identity as one of their keys, with roles being either manager, action or encryption. Now it can start using that information to sign and execute transactions.
Not all transactions need to be on chain, actually most common uses of signed messages should be off chain. If you have a chat app, for instance, you can use the local key for signing messages and sending it to the other parties, and they can just query the identity contract to see if that key actually comes from the user. If you have a game with funds at stake, only the first transaction moving funds and setting up the initial game needs to be executed by the identity: at each turn the players can sign a hash of the current state of the board and at the end, the last two plays can be used to determine the winner. Notice that keys can be revoked at any time, so your app should take that in consideration, for instance saving all keys at the start of the game. Keys that only need this lower level of privilege, should be set at level 4 (encryption).
Once you decided you actually need an on-chain transaction, follow these steps:
from
is the compatible contract you want the transaction to be deployed from.to
and from
fields are the same contract, ie, if the transaction requires the identity to act upon itself (for instance, when adding or removing a key) then you need level 1 (management), otherwise it's 2 (action). Verify if the key your app owns correspond to the required level.requiredSignatures(uint level)
on the target contractAfter having all the signed executable message, we need to deploy it to the chain. If the transaction only requires a single signature, then the app provider can deploy it themselves. Send the transaction to the from
address and attempt to call the function executeSigned
, using the parameters and signature you just collected.
If the transaction need to collect more signatures or the app doesn't have a deployable server, the app should follow these steps:
The goal is to keep broadcasting signatures via whisper until a node that is willing to deploy them is able to collect all messages.
Once you've followed the above steps, watch the transaction pool to any transaction to that address and then take the user to your app. Once you seen the desired transaction, you can stop showing the QR code and proceed with the app, while keeping some indication that the transaction is in progress. Subscribe to the event ExecutedSigned
of the desired contract: once you see the transaction with the nonce, you can call it a success. If you see a different transaction with the same or higher nonce (or timestamp) then you consider the transaction permanently failed and restart the process.
No working examples of this implementation exists, but many developers have expressed interest in adopting it. This section will be edited in the future to reflect that.
This scheme would allow much more lighter apps, that don’t require holding ether, and can keep unlocked private keys on the device to be able to send messages and play games without requesting user prompt every time. More work is needed to standardize common decentralized messaging protocols as well as open source tools for deployment nodes, in order to create a decentralized and reliable layer for message deployment.
Copyright and related rights waived via CC0.