Universal login / signup using ENS subdomains
Alex Van de Sande <email@example.com>
191, 681, 725, 1077
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 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 which implements both Identity and the Executable Signed Messages ERCs. The user should not need 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:
1) Request a name from the user
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.
2.a) Create a new identity
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:
- Generate a private key which it will keep saved locally on the device or browser, the safest way possible.
- Create (or set up) an identity contract which supports both ERC720 and ERC1077
- Register the private key created on step 1 as the only admin key of the contract (the app must not add any app-controlled key, except as recovery option - see 5)
- Register the requested subdomain and transfer its ownership to the contract (while the app controls the main domain and may keep the option to reassign them at will, the ownership of the subdomain itself should belong to the identity, therefore allowing them to transfer it)
- (Optionally) Register a recovery method on the contract, which allows the user to regain access to the contract in case the main key is lost.
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 the 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.
2.b) Connect to an existing identity
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:
- Generate a private key which it will keep saved locally on the device or browser, the safest way possible.
- Query ens to figure the existing address of the identity
- Generate the bytecode for a transaction calling the function
- Broadcast a transaction request on a whisper channel or some other decentralized network of peers. Details on this step require further discussions
- If web3 is available then attempt calling web3.eth.sendTransaction. This can be automatic or prompted by user action.
- Attempt calling a URI if the app supports URL format for transaction requests EIP then attempt calling this. This can be automatic or prompted by user action.
- Show a QR code: with an EIP681 formatted URL. That QR code can be clickable to attempt to retry the other options, but it should be done last: if step 1 works, the user should receive a notification on their compatible device and won't need to use the QR code.
Here's an example of a EIP681 compatible address to add a public key generated locally in the app:
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.
3) Request transactions
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:
- Figure out the TO, FROM, VALUE and DATA. These are the basics of any ethereum transaction.
fromis the compatible contract you want the transaction to be deployed from.
- Check the privilege level you need: if the
fromfields 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.
- Verify how many keys are required by calling
requiredSignatures(uint level)on the target contract
- Figure out gasLimit: Estimate the gas cost of the desired transaction, and add a margin (recommended: add 100k gas)
- Figure out gasToken and gasPrice: Check the current gas price considering network congestions and the market price of the token the user is going to pay with. Leave gasToken as 0 for ether. Leave gasPrice as 0 if you are deploying it yourself and subsidizing the costs elsewhere.
- Sign an executable signed transaction by following that standard.
After 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:
- Broadcast the transaction on a whisper channel or some other decentralized network of peers. Details on this step require further discussions
- If web3 is available then attempt calling web3.eth.personal_sign. This can be automatic or prompted by user action.
- Show a QR code: with the signed transaction and the new data to be signed. That QR code can be clickable to attempt to retry the other options, but it should be done last: if step 1 works, the user should receive a notification on their compatible device and won't need to use the QR code.
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 QRcode 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.
Conclusion and future improvements
This scheme would allow much more lighter apps, that don't require to hold 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.