Wallet-based Passwordless Login
Wallets are decentralized, and the accounts are controlled by the users themselves. Therefore, implementing passwordless login based on wallets is a more secure and user-friendly approach. It not only protects user privacy but also facilitates the development and use of businesses.
TIP
One of the benefits of using wallet-based passwordless login is the robustness and cryptographic security of the decentralized account system, as well as the cost savings of traditional development services (such as SMS verification codes, email verification, etc.) that are no longer needed.
1. Connect Wallet to Retrieve and Verify Address
Use the wallet account address as the user ID. The address can be directly obtained through the wallet API. Refer to DApp Development and Provider APIs
.
After obtaining the wallet address, you need to use the getAddress
method of the ethers
library to verify the validity of the address.
import { useWeb3ModalAccount } from "@web3modal/ethers/react";
import { getAddress } from "ethers";
const { address, isConnected } = useWeb3ModalAccount();
const checksumAddress = getAddress(address);
import { useWeb3ModalAccount } from "@web3modal/ethers/react";
import { getAddress } from "ethers";
const { address, isConnected } = useWeb3ModalAccount();
const checksumAddress = getAddress(address);
2.Nonce handling
The server should also verify the validity of the address.
You can use the string + random num strategy to generate random data to be signed:
import * as ethUtil from "ethereumjs-util";
if (!ethUtil.isValidChecksumAddress(address)) {
throw new HttpException("Invalid address", HttpStatus.BAD_REQUEST);
}
const nonceMessage = `Verify account ownership, checksumAddress: ${address}, nonce: ${Math.random()
.toString(36)
.substring(2, 15)}`;
import * as ethUtil from "ethereumjs-util";
if (!ethUtil.isValidChecksumAddress(address)) {
throw new HttpException("Invalid address", HttpStatus.BAD_REQUEST);
}
const nonceMessage = `Verify account ownership, checksumAddress: ${address}, nonce: ${Math.random()
.toString(36)
.substring(2, 15)}`;
The frontend requests the server, reports the address, and retrieves the nonce assigned by the server corresponding to that address.
const nonce = await getNonceApi.request({ address: checksumAddress });
const nonce = await getNonceApi.request({ address: checksumAddress });
3. Signing and Verification
The server-side implements the login logic, verifying the legitimacy of the address, signature, and nonce.
import * as ethUtil from "ethereumjs-util";
const validateSignature = async (nonce, signature, publicAddress) => {
const msgBf = ethUtil.toBuffer(Buffer.from(nonce));
const msgHash = ethUtil.hashPersonalMessage(msgBf);
const signPrm = ethUtil.fromRpcSig(signature);
const publicKey = ethUtil.ecrecover(msgHash, signPrm.v, signPrm.r, signPrm.s);
const addressBf = ethUtil.publicToAddress(publicKey);
const address = ethUtil.bufferToHex(addressBf);
return address.toLowserCase() === publicAddress.toLowerCase();
};
if (!(await validateSignature(nonce, signature, address))) {
throw new HttpException("Invalid signature", HttpStatus.BAD_REQUEST);
}
// DB Query address
// JWT logic ...
import * as ethUtil from "ethereumjs-util";
const validateSignature = async (nonce, signature, publicAddress) => {
const msgBf = ethUtil.toBuffer(Buffer.from(nonce));
const msgHash = ethUtil.hashPersonalMessage(msgBf);
const signPrm = ethUtil.fromRpcSig(signature);
const publicKey = ethUtil.ecrecover(msgHash, signPrm.v, signPrm.r, signPrm.s);
const addressBf = ethUtil.publicToAddress(publicKey);
const address = ethUtil.bufferToHex(addressBf);
return address.toLowserCase() === publicAddress.toLowerCase();
};
if (!(await validateSignature(nonce, signature, address))) {
throw new HttpException("Invalid signature", HttpStatus.BAD_REQUEST);
}
// DB Query address
// JWT logic ...
The frontend receives the nonce and calls the wallet's signing method. It then uses the resulting signature and address to log in.
import { BrowserProvider } from "ethers";
async function onSignMessage(msg) {
const provider = new BrowserProvider(walletProvider);
const signer = await provider.getSigner();
const signature = await signer.signMessage(msg);
return signature;
}
const signature = onSignMessage(nonce);
const jwt = await loginApi.request({
address: checksumAddress,
signature,
nonce,
});
// updateLoginState() ...
import { BrowserProvider } from "ethers";
async function onSignMessage(msg) {
const provider = new BrowserProvider(walletProvider);
const signer = await provider.getSigner();
const signature = await signer.signMessage(msg);
return signature;
}
const signature = onSignMessage(nonce);
const jwt = await loginApi.request({
address: checksumAddress,
signature,
nonce,
});
// updateLoginState() ...
After that, you can use JWT (JSON Web Tokens) for communication between the frontend and backend.