4 minutes read
User authentication via Metamask & Portis
User authentication via external blockchain wallets such as MetaMask, Portis. Instead of using the traditional auth method where you enter a username and password.
Michał Mirończuk
Introduction 💬
Applications that use blockchain wallets often face a problem with user authentication. Instead of using the traditional authentication method where the user has to provide a username and password, the user can use external wallets such as MetaMask or Portis. They provide the low level features of each wallet the ability to sign a specific message that will help us recognize and verify the user. In this post, I will walk you through a simple solution and give you a basic user authentication implementation the above wallets and ethereum cryptographic libraries together with JSON Web Token (JWT). If you are not familiar with JWT, please visit this website.
In general, the whole process consists of 6 steps:
Client
- Establishing a message on which we will work
- Signing message by the user (using private key implicitly)
- Sending signature and user address to the server
Server
- Verifying signed message
- Generating JWT
- Sending access token to the user
Diagram 📌
As I am an advocate of presenting knowledge in a graphical way, I have prepared the sequence diagram below, which will help us in better understanding of the dataflow between parties.
Implementation 🔥
Because of big interested in NestJS, I have used this framework for backend side. Progressive framework facilities building efficient, scalable Node.js server-side applications and provides many embedded modules like @nestjs/jwt package. For client application I have used React as the most popular solution for building frontend side.
The entire implementation of this demo has been uploaded to the GitHub repository on my profile here.
Client 📍
Major part of client code - signing the transaction is in the LoginComponent
in authenticate
method
authenticate = async () => {
const accountAddress = this.props.account;
const signature = await this.props.web3.currentProvider.send('personal_sign', [
getMessage(),
accountAddress, //from which account should be signed. Web3, metamask will sign message by private key inconspicuously.
]);
const signatureResult = signature.result;
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountAddress, signature: signatureResult }),
};
//make request to local server
return fetch('http://localhost:3001/auth/login', requestOptions)
.then((response) => response.json())
.then((token) => {
this.setState({ access_token: token });
});
};
Thanks to extended web3 library we have an access to many functions. To sign a message we have took adventage of personal_sign
method where we need to pass two arguments:
- message - data which we are signing, has to be the same and common for the client app as well as for the server
- account address - from which account should be signed. Web3 library will sign message by private key implicitly. 💡
Isn't it easy to generate a signature? 🚀🚀🚀
Now we are able to send the signature together with wallet address to our server 😄
Let's move on to the back-end side where the verification operations will be performed on the secure side.
Server 📍
If we look at the diagram, we are reminded that the backend has to recover the address and compare it with the one sent. Therefore, our application service comes into play with the code below.
@Injectable()
export class AppService {
constructor(private readonly jwtService: JwtService) {}
loginUser(loginDto: LoginDto): string {
const { accountAddress, signature } = loginDto;
var recoveredAddr;
try {
recoveredAddr = recoverPersonalSignature({
data: getMessage(),
sig: signature,
});
} catch (err) {
throw new HttpException('Problem with signature verification.', 403);
}
if (recoveredAddr.toLowerCase() !== accountAddress.toLowerCase()) {
throw new HttpException('Signature is not correct.', 400);
}
//save your user here (i.e var user = await this.usersService.createWalletAccountIfNotExist(createUserDto);)
const payload: JwtUser = {
account_address: accountAddress,
};
const access_token = this.jwtService.sign(payload);
return access_token;
}
}
On input, the service takes the wallet address and signature. To recover the address, the service must use the function called recoverPersonalSignature
provided by eth-sig-util
library. The function also requires a shared message but this time the signature is passed instead of the address. ✔️
In the next step is to compare the recovered address with the address received from the user. If they are not equal, an exception is thrown. ✔️
Finally JwtService is injected into AppService in order to generate new access token for the user. ✔️
Different approach? 🤔
A slightly more advanced and secure solution may be that the server generates an initial message that will require the user to sign this particular message.
Tags:
Get Free
Quote
Sign Up For Newsletter
Receive 50% discount on first project