Web3 Authentication: What is it? What Use Can It Have?

Unlocking the Benefits of Decentralized Authentication with Web3 Technology

Williams Peter
Coinmonks
Published in
10 min readMar 29, 2023

--

Currently, Web3 (sometimes referred to as Web 3.0 or Web3) is a hot topic on the Internet. Despite claims to the contrary, what does Web3 actually entail? How is it applicable today? We have received many inquiries. Now that we have everything out of the way, let’s look at Web3 and authentication using it.

Web3: What Is It? How Does Web3 Work?

Web1-Web2-Web3

The internet has significantly changed over the past years as a result of its development. With Web 1.0, we advanced from using static websites to dynamic ones, then talked about the rich Internet applications of Web 2.0, and now with Web 3.0, we are living in the age of the Internet of Things and distributed computing.

Web 3.0, sometimes referred to as the "third generation" of the World Wide Web, is based on distributed computing and the Internet of Things. Smart homes and connected devices are becoming the order of the day in this era, and decentralized applications are being built using blockchain technology. This generation is focused on giving consumers more control over their data while providing a more personalized and immersive experience.

Many individuals would benefit from this decentralization since it would make it simpler to access the Internet from any location. Artificial intelligence (AI) would restrict both misuse and abuse of the system, which is one of the promises made simultaneously.

What is Web3 Authentication?

Web3 Authentication is a form of authentication based on the emerging technologies of blockchain, digital signatures, and distributed ledgers. It is a form of authentication that is designed to be more secure than traditional authentication methods and provide a higher level of security. Despite its fancy-sounding name, it is only a login mechanism. Web3 applications utilize your crypto addresses to authenticate you, whereas the majority of our current applications use a combination of email and password. Blockchain networks are secure because of cryptography, but users must navigate a far more complex system. Dealing with public and private key pairs would be necessary for manual login, which is not at all user-friendly.

Thankfully, there are many trustworthy crypto wallets available as browser extensions and mobile apps. These wallets can be used as Web3 authentication tools in addition to managing and storing coins.

Why Is Web3 Authentication Necessary?

Users have the opportunity to engage with certain blockchains using Web3 apps, as previously mentioned. Yet, users should be able to connect to these crypto networks in a secure manner. Users can join a network of their choice using the Web3 authentication technique. After they have authenticated, they can communicate with other network users who have also done so.

There are several reasons why Web3 authentication is superior to social logins and email/password authentication via the blockchain, which may have problems and is still in its early stages.

Pros

  • Enhanced privacy: No third party is involved, and no email is required.
  • Enhanced security: Public key encryption makes property evidence more secure than proof of ownership by email, password, or third party. By storing the credentials locally on your device rather than on a server, Web3 wallets (or authorizing tools) can reduce the attack surface.
  • Simplified User Experience: You don’t need to write down or remember any passwords to complete this login flow; you may complete it in a matter of seconds with a single click.

Cons

  • A user authentication application is required: Without the Web3 authorization application, this authorization flow plainly fails (like MetaMask). Convincing consumers to download this program should be challenging, and creating this application can be fairly expensive.
  • As you can see, it’s really easy to implement. Nevertheless, it needs certain adjustments in every area that affects authentication, including signup, database, authentication routes, etc., to be integrated into an already complicated system.

The Authorization Flow: How Does It Work?

Photo by Markus Spiske on Unsplash

Modify the User Model at the Backend

The publicAddress and nonce fields in our User model should be added first. Also, each account’s publicAddress needs to be distinct. If you wish to design a login process in addition to an email and password, you can maintain the standard username, email, and password fields.

The signup process will also be a little bit different; the publicAddress field will now be required. You can be sure that the user will never have to manually type their publicAddress because web3.eth.coinbase can retrieve it.

const User = sequelize.define<UserInstance, UserAttributes>('User', {
nonce: {
allowNull: false,
type: Sequelize.INTEGER.UNSIGNED,
defaultValue: () => Math.floor(Math.random() * 1000000) // Initialize with a random nonce
},
publicAddress: {
allowNull: false,
type: Sequelize.STRING,
unique: true,
validate: {
isAlphanumeric: true,
isLength: {
args: 8,
msg: 'Public address should be 8 characters long'
}
}
},
username: {
type: Sequelize.STRING,
unique: true,
validate: {
notEmpty: true,
}
}
});

interface UserAttributes {
nonce: number;
publicAddress: string;
username: string;
}

interface UserInstance extends Sequelize.Instance<UserAttributes> {
id: number;
createdAt: Date;
updatedAt: Date;
}

PublicAddress and nonce are the two fields that are needed. Nonce started out with a random number. After each successful login, this number has to be updated. Today’s JWTs come to mind when we think of nonces.

Create Nonces on the Backend

The nonce column should be filled with a random value for each user in the database. Nonce, for instance, may be a large random integer.

This is done in the defaultValue() function in the model definition above.

User Fetches Their Nonce at the Frontend

In our typescript code, we have access to window.web3. We can call web3.eth.coinbase to get the current account’s public address.

We send an API request event to the backend when a user hits the login button in order to get the nonce linked to their public address. It should be possible to perform this using a route with the filter argument GET /api/users?publicAddress=$publicAddress. Naturally, the backend should be set up to only display public information (including the nonce) on this route, as this is an unauthenticated API request.

When a prior request yields no results, it signifies that the current public address hasn't been registered yet. By using POST /users and including publicAddress in the request body, we must first establish a new account. On the other hand, if a result is present, we save it as a nonce.

Our handleClick() handler does the following actions when a user hits the login button:

const Login: React.FC = () => {
const handleClick = () => {
const publicAddress = web3.eth.coinbase.toLowerCase();

// Check if user with current publicAddress is already present on back end
fetch(`${process.env.REACT_APP_BACKEND_URL}/users?publicAddress=${publicAddress}`)
.then(response => response.json())
// If yes, retrieve it. If no, create it.
.then(
users => (users.length ? users[0] : handleSignup(publicAddress))
)
};

const handleSignup = (publicAddress: string) =>
fetch(`${process.env.REACT_APP_BACKEND_URL}/users`, {
body: JSON.stringify({ publicAddress }),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
}).then(response => response.json());

return (
<div>
<button type="button" onClick={handleClick}>Login</button>
</div>
);
};

export default Login;

With web3.eth.coinbase, we are fetching the current account in this instance. The next step is to determine if the backend contains an existing copy of this publicAddress. In the handleSignup() function, if the user already exists, we either obtain it or create a new account.

The user Signs the Nonce at the Frontend

The following code is executed by the frontend after it has received the nonce from the previous API call’s response:

web3.personal.sign(nonce, web3.eth.coinbase, (error: any, result: any) => {
callback(error, result);
});

This will launch MetaMask, a browser plugin created to make it simpler to access Ethereum’s Dapp ecosystem. It also functions as a wallet for ERC-20 tokens, enabling users to access network services by using the wallet to display a confirmation popup for message signing. This popup will display the nonce so the user may be sure they aren’t signing any dangerous material.

The signed message (called the signature) will be passed as an argument when the callback function is invoked after they accept it. The frontend then issues a second POST /api/authentication API request, passing a body that has both a signature and a publicAddress.

We currently possess a user that was provided by the backend (be it retrieved or newly created). Their publicAddress and nonce are available. We can now use web3.personal.sign to sign the nonce using the private key connected to this publicAddress. The handleSignMessage() method performs this.

A hexadecimal representation of the string is sent as the first argument to web3.personal.sign. We must use web3.fromUtf8 to translate our UTF-8-encoded text into hexadecimal representation. Also, as it will be seen in the MetaMask confirmation window, we can choose to sign the following rather than just the nonce:

const Login: React.FunctionComponent = () => {
const handleClick = () => {
fetch(`${process.env.REACT_APP_BACKEND_URL}/users?publicAddress=${publicAddress}`)
.then(response => response.json())
// If yes, retrieve it. If no, create it.
.then(
users => (users.length ? users[0] : handleSignup(publicAddress))
)
// Popup MetaMask confirmation modal to sign message
.then(handleSignMessage)
// Send signature to back end on the /auth route
.then(handleAuthenticate)
};

const handleSignMessage = ({ publicAddress, nonce }) => {
return new Promise((resolve, reject) =>
web3.personal.sign(
web3.fromUtf8(`I am signing my one-time nonce: ${nonce}`),
publicAddress,
(err, signature) => {
if (err) return reject(err);
return resolve({ publicAddress, signature });
}
)
);
};

const handleAuthenticate = ({ publicAddress, signature }) =>
fetch(`${process.env.REACT_APP_BACKEND_URL}/auth`, {
body: JSON.stringify({ publicAddress, signature }),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
}).then(response => response.json());

return (
<div>
<button onClick={handleClick}>Login</button>
</div>
);
}

export default Login;

We proceed to the handleAuthenticate() function once the user has properly signed the message. We only make a request to the backend’s /auth route, including our publicAddress and the signature from the message the user has just signed.

Backend Signature Verification

When the backend gets a POST /api/authentication request, it first retrieves the user in the database corresponding to the publicAddress specified in the request body. It retrieves the related nonce.

The backend may cryptographically confirm that the nonce has been properly signed by the user by using the nonce, public address, and signature. If so, the user has established ownership of the public address and is therefore considered authenticated. The front end can then get a JWT or session identification.

The slightly more challenging component is this. On the /auth route, the backend gets a request with a publicAddress and a signature; it must then confirm that this publicAddress has signed the right nonce.

As we designated publicAddress as a unique field in the database, there is only one person with that publicAddress, thus the first step is to get him or her from the database. The message msg was then set to “I am signing my...”, just as it had been in Step 04’s frontend, with these users’ nonce.

The actual verification is the next block. There is some use of cryptography.

This block, our provided message (which contains the nonce), and our signature are all put together, and the ecrecover() method returns the public address that was used to sign the message. If the publicAddress matches the one in the request body, the person who submitted the request has successfully established their ownership of the publicAddress. We see them as authentic.

  User.findOne({ where: { publicAddress } })
.then(user => {
const msg = `I am signing my one-time nonce: ${user.nonce}`;

// We now are in possession of msg, publicAddress and signature. We can perform an elliptic curve signature verification with ecrecover
const msgBuffer = ethUtil.toBuffer(msg);
const msgHash = ethUtil.hashPersonalMessage(msgBuffer);
const signatureBuffer = ethUtil.toBuffer(signature);
const signatureParams = ethUtil.fromRpcSig(signatureBuffer);
const publicKey = ethUtil.ecrecover(
msgHash,
signatureParams.v,
signatureParams.r,
signatureParams.s
);
const addressBuffer = ethUtil.publicToAddress(publicKey);
const address = ethUtil.bufferToHex(addressBuffer);

// The signature verification is successful if the address found with ecrecover matches the initial publicAddress
if (address.toLowerCase() === publicAddress.toLowerCase()) {
return user;
} else {
return res
.status(401)
.send({ error: 'Signature verification failed' });
}
} as Promise<User>)

The backend produces a JWT and delivers it back to the client after successful authentication. This authentication method is traditional.

Change the Nonce at the Backend

We make sure that the user must sign a fresh nonce the next time they wish to log in in order to prevent them from using the same signature (in case it is hacked) to log in. For this purpose, a new random nonce is created and stored in the database for this user.

The nonce must be changed as a last precaution for security.

.then(user => {
user.nonce = Math.floor(Math.random() * 1000000);
return user.save();
})

Conclusion

Photo by Lama Roscu on Unsplash

Users may be easily authenticated thanks to software wallets. Everything is taken care of for us by the wallet; all we have to do is confirm a signature. There are no recovery questions to keep track of or credentials to handle.

It wasn’t that difficult, right? Please feel free to have a look at the GitHub source to see how the entire application is hooked up (including JWT creation, CRUD routes, localStorage, etc.).

Thanks for reading.

New to trading? Try crypto trading bots or copy trading on best crypto exchanges

Join Coinmonks Telegram Channel and Youtube Channel get daily Crypto News

Also, Read

--

--

Williams Peter
Coinmonks

Ex-CEO at Kosmero | FullStack Engineer (MERN) | Web2 | Web3 Frontend Engineer | Technical Writer | Developer Relations