An Introduction to Ethereum and Smart Contracts: A Programmable Blockchain, Part 4
Welcome to the fourth and final part of our 'A Programmable Blockchain' series! Read on to learn how to make an actual app using the Ethereum Blockchain.
Join the DZone community and get the full member experience.
Join For FreeWelcome back! If you missed Part 1, Part 2, or Part 3 of our 'A Programmable Blockchain' series, follow these links to check them out!
A Simple Login System Using Ethereum
One of the cool things about Ethereum is that addresses are, by definition, systems to prove ownership. Whoever can perform operations with an Ethereum address is the rightful owner of that address. This is, of course, the consequence of the underlying public-key infrastructure used to verify transactions. We can exploit this to create a login system based on Ethereum addresses. Let's see how.
Any login system is mainly concerned with creating a unique identity that can be managed by whoever can pass a certain "login challenge." The login challenge is the method to prove that the same entity that created the account in the first place is the same entity doing operations now. Most systems rely on the classic username + password login challenge: a new user registers by choosing a unique username and a password, then, anytime the system requires proof that the user is in fact who he says he is, it can request the password for that username. This system works. But with Ethereum we already have a system for proving identities: public and private keys!
We'll design a simple contract that can be used by any user to validate his ownership of an address. The login process will be as follows:
- A user accesses a website that requires him or her to log in. When the user is not logged in, the website requests the user to enter his or her Ethereum address.
- The backend for the website receives the address for the user and creates a challenge string and a JWT. Both of these are sent back to the user.
- The user sends the challenge string to the
Login
contract and stores the JWT for later use locally. - The backend listens for login attempts using the challenge string at the Ethereum network. When an attempt with the challenge string for the right user is seen, it can assume the user has proved his or her identity. The only person that can send a message with an Ethereum address is the holder of the private key, and the only user that knows the challenge string is the user that received the challenge through the login website.
- The user gets notified or polls the website backend for confirmation of his or her successful login. The user then proceeds to use the JWT issued in step 2 for accessing the website. Alternatively, a new JWT can be issued after a successful login.
To that end, this is the Ethereum contract we will use:
pragma solidity ^0.4.2;
contract Login {
event LoginAttempt(address sender, string challenge);
function login(string challenge) {
LoginAttempt(msg.sender, challenge);
}
}
The contract is extremely simple. Events
are special elements in Solidity that are mapped to a system in Ethereum that allows special data to be logged. Events are generally watched by clients monitoring the evolution of the blockchain. This allows actions to be taken by clients when events are created. In our case, whenever a user attempts to log in, an event created with the challenge is broadcast. We only care about receiving a call from the rightful owner of the Ethereum address that was passed to the third party website. And, thanks to the way Ethereum works, we can be sure the sender was the one who performed the call.
In addition to the sender's address, the challenge is also broadcast. This means anyone watching the blockchain now knows the challenge. However, this cannot be used on its own to impersonate a user: a user can only interact with the backend through the session JWT. This means an attacker must know three pieces of information to impersonate a user: the Ethereum address, the challenge, AND the JWT issued with the challenge. Since JWTs are signed, an attacker cannot create a valid JWT to impersonate a user, even with access to the challenge.
What follows is our backend code. First, let's see how to watch for Ethereum events:
const LoginContract = require('./login_contract.js');
const loginContract = LoginContract.at(process.env.LOGIN_CONTRACT_ADDRESS ||
'0xf7b06365e9012592c8c136b71c7a2475c7a94d71');
// LoginAttempt is the name of the event that signals logins in the
// Login contract. This is specified in the login.sol file.
const loginAttempt = loginContract.LoginAttempt();
const challenges = {};
const successfulLogins = {};
loginAttempt.watch((error, event) => {
if(error) {
console.log(error);
return;
}
console.log(event);
const sender = event.args.sender.toLowerCase();
// If the challenge sent through Ethereum matches the one we generated,
// mark the login attempt as valid, otherwise ignore it.
if(challenges[sender] === event.args.challenge) {
successfulLogins[sender] = true;
}
});
The login_contract.js
file contains what is needed to inter-operate with our contract. Let's take a look:
// web3 is an Ethereum client library
const Web3 = require('web3');
const web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545'));
// This file is generated by the Solidity compiler to easily interact with
// the contract using the web3 library.
const loginAbi = require('../solidity/build/contracts/Login.json').abi;
const LoginContract = web3.eth.contract(loginAbi);
module.exports = LoginContract;
Web3 is the official client library to interact with Ethereum nodes. An Ethereum node is what actually connects to the rest of the Ethereum network. It performs "mining" (block generation), transaction operations (create and send) and block verification.
The Login.json
file is generated by the Solidity contract compiler, part of the standard Ethereum development tools. The Solidity compiler takes Solidity source code and turns it into Ethereum Virtual Machine bytecode and an interface description file that can be used by Web3 to interact with the contract once it is uploaded to the network.
And here are our HTTP endpoints:
app.post('/login', (req, res) => {
// All Ethereum addresses are 42 characters long
if(!req.body.address || req.body.address.length !== 42) {
res.sendStatus(400);
return;
}
req.body.address = req.body.address.toLowerCase();
const challenge = cuid();
challenges[req.body.address] = challenge;
const token = jwt.sign({
address: req.body.address,
access: 'finishLogin'
}, secret);
res.json({
challenge: challenge,
jwt: token
});
});
app.post('/finishLogin', validateJwt, (req, res) => {
if(!req.jwt || !req.jwt.address || req.jwt.access !== 'finishLogin') {
res.sendStatus(400);
return;
}
if(successfulLogins[req.jwt.address]) {
delete successfulLogins[req.jwt.address];
delete challenges[req.jwt.address];
const token = jwt.sign({
address: req.jwt.address,
access: 'full'
}, secret);
res.json({
jwt: token,
address: req.jwt.address
});
} else {
// HTTP Accepted (not completed)
res.sendStatus(202);
}
});
app.post('/apiTest', validateJwt, (req, res) => {
if(req.jwt.access !== 'full') {
res.sendStatus(401); //Unauthorized
return;
}
res.json({
message: 'It works!'
});
});
The /login
endpoint receives a login request carrying an Ethereum address for the user that wants to log in. The user must be the owner of such an Ethereum address. It generates a JWT and a challenge. The JWT can only be used to access the /finishLogin
endpoint.
Before the user can call the /finishLogin
endpoint he or she must prove his or her identity by making a call to the login
method of the Login
contract. The login
method receives a single parameter: the challenge returned by the /login
endpoint. He must perform this call using the same account address that was passed to the /login
endpoint. He or she can use any Ethereum wallet or client to do this.
After making the call to the login
method of the Login
contract, the user can complete the login by using the /finishLogin
endpoint. He or she must pass the JWT returned by the /login
endpoint to it. If the login is successful, a new JWT with full access is returned. Otherwise, if the login is still pending, an accepted HTTP status (202) is returned signaling proper verification of the login request is still pending. If the JWT passed to /finishLogin
is invalid, an unauthorized HTTP status code is returned (401).
After the /finishLogin
endpoint is called and the login process is completed, the returned JWT can be used to access other parts of the API. In this case, the /apiTest
endpoint is available. It simply returns "It works!" wrapped in a JSON object if the user is logged-in.
Running the Example
Building and deploying the example is not as straightforward as it may seem due to the nature of Ethereum and current development tools. Here are the steps we used to test the example above.
1. Get an Ethereum Node Client
There are several Ethereum node clients. A popular one is go-ethereum, a client written in Go. Download it and install it.
Ethereum, as other cryptocurrencies do, has different versions of the blockchain with different parameters. There are essentially two blockchains: the main official blockchain and a test blockchain. The main blockchain never undoes operations once they are confirmed. Since some operations require money, the main blockchain is not ideal for testing. The test blockchain, on the other hand, is much less strict about forks and changes. It is also simpler to mine "Ether," Ethereum's currency.
We could use the test network for our example here. However, running a client node for any of the public networks is problematic for one reason: to be able to start doing transactions, the client must first verify all previous transactions in the blockchain. That means that bootstrapping a new client node takes quite a bit of time. Fortunately, there is an alternative: we can create a new, pristine private Ethereum blockchain to run our tests. To do so, run go-ethereum using the following command line:
./geth --rpc --nat none --dev
2. Create a New Ethereum Account to Mine Some Ether
The geth
command can also be used to interact with a running client. Launch an interactive console connected to the running client:
/geth attach ipc:/var/folders/ts/7xznj_p13xb7_5th3w6yjmjm0000gn/T/ethereum_dev_mode/geth.ipc
The IPC file mentioned in the command can be found in the output from running the node in our first step. Look for the line that reads:
IPC endpoint opened: /var/folders/ts/7xznj_p13xb7_5th3w6yjmjm0000gn/T/ethereum_dev_mode/geth.ipc
Now in the Geth console type:
personal.newAccount()
After hitting ENTER
a prompt will appear requesting a passphrase. This is the passphrase that will be used to perform any operations using this account. You can think of this as the passphrase required to decrypt the private key used to sign Ethereum transactions. Do not leave the prompt empty, choose a simple passphrase for testing instead. A new Ethereum address will be returned by the function. If at any point you forget this address, you can list accounts by inspecting personal.listAccounts
(it's a variable, not a function, so don't add ()
at the end).
3. Start Mining Some Ether
Now it's time to add some Ether to our new account. Ether is required to perform operations in the Ethereum blockchain, so it is necessary to perform this step. Ether can be gathered in two ways: by receiving it from another account or by mining it. Since this is a private network, we will need to mine it. Don't worry, the private network is by default configured to be able to mine Ether easily. Let's do it:
miner.setEtherbase(personal.listAccounts[0]) // Hit ENTER
miner.start() // Hit ENTER
Now wait a few seconds (or minutes depending on your hardware) and then confirm you have some Ether in your account:
eth.getBalance(personal.listAccounts[0]) // Hit ENTER
4. Compile and Deploy Our Login Contract
To simplify the process of compiling and deploying contracts, we will use truffle
. Truffle is a development framework for Ethereum, simplifying many common tasks. Install it:
npm install -g truffle
Before using truffle to deploy contracts, it is necessary to "unlock" our account in our Ethereum node client. Unlocking is the process of decrypting the private key and holding it in memory using the passphrase used to create it. This allows any client libraries (such as Truffle) connecting to the node to make operations on behalf of the unlocked account. Go to the geth
console and type:
personal.unlockAccount(personal.listAccounts[0]) // Hit ENTER
Now switch to the solidity
directory of our sample application. Edit the truffle.js
file and set your newly created address as the from
key. Then run:
truffle migrate
The migrate
command compiles and deploys the contracts to the Ethereum network on behalf of the account set in truffle.js
. As a result, you will get the address of the newly deployed contract. Take note of it.
5. Install an Ethereum Wallet
Ethereum wallets are convenient interfaces for users to interact with the Ethereum network. Sending and receiving Ether, deploying contracts or making calls to them are all operations usually supported by wallets. Mist is the official Ethereum wallet. Download it and install it.
Once installed, we will need to tell Mist to connect to our private network rather than the public main or test networks. To do this, run Mist from the command line like so:
./Ethereum\ Wallet --rpc /var/folders/ts/7xznj_p13xb7_5th3w6yjmjm0000gn/T/ethereum_dev_mode/geth.ipc
The IPC file is the same file used by the geth
console and can be gathered from the geth
output logs.
6. Tell the Ethereum Wallet of the Contract
Many contracts live in the Ethereum network. Wallets need to know a contract's address and interface before being able to interact with them. Let's tell Mist about our Login contract. Go to Contracts -> Watch Contract
(top right, then bottom left).
Complete the fields as follows:
- Name: Login
- Contract Address:
- JSON Interface: the abi from
Login.json
. For convenience, it is pasted below. Copy and paste it in Mist.
As a test, now try to send some Ether to the Contracts -> Login -> Transfer Ether & Tokens
contract:. Send1 Ether
or any other amount less than your balance. You will need to provide the passphrase for your account.
7. Deploy the Backend
Go to the backend
folder and run:
npm install
node app.js
8. Serve the Frontend
Go to the frontend
folder and run:
npm install -g static-serve
static-serve
You may use any other simple static HTTP server such as Python's SimpleHTTPServer
. If you do so, make sure to serve the app in port 9080. This is important due to CORS.
9. Test Everything Together!
Open your browser at http://localhost:9080. Now attempt to login by putting your Ethereum address in the input field. A challenge text will be generated. Go to the Mist (Ethereum Wallet) and go to the Login contract. To the right, you will see "WRITE TO CONTRACT." Select the login
function and paste the challenge in the text fill that appears there. Then click onExecute
. Input your passphrase and send the transaction.
Now switch back to the login page. After a few seconds, the login will be completed and a welcome message will appear. Voilà!
This example shows how a typical Ethereum user can use his existing Ethereum account to log in to any third party website supporting Ethereum. And all of this is done without a central server. Although authentication is not performed by the owner of the website, there is no central authority validating the user: it is the Ethereum network that does so.
Conclusion
We have taken a deeper look at Ethereum: a decentralized, blockchain-based framework for developing applications. Applications run on each node, and each state transition produced by them is validated and recorded by the blockchain. The power of the approach extends the concepts of Bitcoin to more than just monetary transactions or simple non-Turing complete contracts. The power of distributed apps is just beginning to be tapped. In the next post in the series, we will take a look at an actual application developed on the Ethereum network: a two-factor authentication system for Ethereum users using a mobile validator application. Stay tuned!
Published at DZone with permission of Sebasti%|-1584275889_1|%n Peyrott, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments