Я делаю dapp, который будет звонить на сервер Node.js. Я ожидаю, что у пользователя будет установлен MetaMask, и я хочу убедиться, что он является реальным владельцем текущего адреса в MetaMask (т.е. accounts[0]
).
Это пользовательский поток, который я пытаюсь реализовать:
accounts[0]
от Web3/MetaMask.accounts[0]
моему Node.js API.accounts[0]
. Если это действительно так, то я отвечаю конкретными данными.Я долго изучал различные функции подписи в Web3 и в итоге очень запутался. Есть:
web3.eth.sign
- recover
аналога этому нет, и MetaMask не выскакивает с просьбой что-то подписать.web3.eth.personal.sign
- для этого требуется пароль, я не хочу просить пользователя вводить пароль, разве MetaMask не должен этого делать?web3.eth.accounts.sign
- это больше похоже на хеш-функцию, чем на то, что мне нужно.У меня такое ощущение, что ни одна из трех вышеперечисленных функций мне не нужна. Может ли кто-нибудь дать некоторые рекомендации о том, как подойти к этому?
Я думаю web3.eth.sign
, это то, что вы хотите, но обратите внимание, что он ожидает 32-байтовую строку (обычно хэш сообщения).
Это сработало для меня:
web3.eth.sign(web3.eth.defaultAccount, web3.sha3('test'), function (err, signature) {
console.log(signature); // But maybe do some error checking. :-)
});
Затем на сервере с помощью ethereumjs-util
:
const util = require('ethereumjs-util');
const sig = util.fromRpcSig('<signature from front end>');
const publicKey = util.ecrecover(util.sha3('test'), sig.v, sig.r, sig.s);
const address = util.pubToAddress(publicKey).toString('hex');
Вы сказали, что «MetaMask не выскакивает с просьбой что-то подписать», но это должно было быть. Если он по-прежнему не работает, поделитесь кодом, который вы используете для вызова web3.eth.sign
.
РЕДАКТИРОВАТЬ
Обратите внимание, что в web3.js 1.0 web3.util.sha3
вместо web3.sha3
.
web3.utils.sha3
вместо web3.sha3
, (2) fromRpcSig()
мне нужно было передать все это с начальным 0x. Если вы добавите это в редактирование, я приму ваш ответ.0x
действительно необходим (независимо от того, какую версию web3.js вы используете). Я отредактировал ответ.Что я могу сказать по своему опыту, так это то, что ни один из приведенных выше ответов не работал у меня из коробки. Вот как я это реализовал (проверено с помощью MetaMask и WalletConnect)
Фронтенд веб-приложение (ReactApp)
Задача состоит в том, чтобы подписать одноразовый номер (случайную строку), предоставленный сервером. Вот как может потребоваться безопасный знак
let sign = await web3.eth.personal.sign(nonce, walletAddress, "")
Где:
web3
является экземпляром web3 (в настоящее время @ 1.3.6)nonce
случайная строка для подписи (передается как простая строка)walletAddress
адрес кошелька в виде строки (например, 0xaabb....ccdd)Созданный знак затем отправляется как есть на серверную часть.
Верификация серверной части
Цель состоит в том, чтобы извлечь из подписи адрес кошелька, подписавшего запрос. Таким образом, нет никакого способа подделать это.
import * as util from "ethereumjs-util";
nonce = "\x19Ethereum Signed Message:\n" + nonce.length + nonce
nonce = util.keccak(Buffer.from(nonce, "utf-8"))
const { v, r, s } = util.fromRpcSig(signature)
const pubKey = util.ecrecover(util.toBuffer(nonce), v, r, s)
const addrBuf = util.pubToAddress(pubKey)
const addr = util.bufferToHex(addrBuf)
Когда мы подписываем одноразовый номер в браузере, перед одноразовым номером автоматически добавляется заполняющее сообщение, поэтому нам нужно заново создать его на стороне сервера, чтобы проверить его.
Здесь мы воссоздаем одноразовый номер, затем извлекаем открытый ключ подписавшего и получаем адрес кошелька из открытого ключа. Если этот адрес совпадает с ожидаемым , пользователь является владельцем закрытого ключа, связанного с извлеченным адресом.
ethereum-js
был пользователем версии 7.0.10 ( пакет npm )
nonce
— это случайная строка для подписи, которая генерируется серверной частью и отправляется во внешний интерфейс для подписи. Signature
является результатом операции со знаком и в нашем случае генерируется с помощью менеджера кошелька, такого как Metamask. Это не просто концепции, связанные с web3. Я предлагаю вам прочитать больше об этой теме, например stackoverflow.com/questions/4751172/…window.web3
устарел в пользу window.ethereum
:window.ethereum.sendAsync({method: 'personal_sign', params: [window.ethereum.selectedAddress, nonce], from: window.ethereum.selectedAddress}, (err, result) => { console.log('signature', result.result) });
Собственный web3 MetaMask, все еще 0.2 по состоянию на май 2018 года, синтаксис знака -
console.log(web3.version.api);
web3.personal.sign(web3.toHex("message to sign"), accounts[0],
function(err, res) {
// whatever
});
Если вы замените web3 MetaMask на web3.js 1.0.0 (бета), используйте следующий синтаксис --
window.web3 = new Web3(web3.currentProvider);
console.log(web3.version);
web3.eth.personal.sign('message to sign', accounts[0])
.then(signature => {
// whatever
});
В обоих случаях MetaMask выведет всплывающее уведомление о подписи.
Я реализовал это, и это прекрасно работает. Я использую Python/Flask в бэкэнде, поэтому вам нужно будет найти эквивалентный бэкэнд-код для Node:
Серверная часть: храните пользователя по его общедоступному адресу в базе данных вместе с одноразовым номером, используемым для входа.
Самая простая схема для Пользователя/Учетной записи:
public_address = db.Column(db.String(80), primary_key=True, nullable=False, unique=True)
nonce = db.Column(
INTEGER(unsigned=True),
nullable=False,
default=generate_nonce,
)
Где generate nonce — это генератор псевдослучайных чисел, например:
def generate_nonce(self, length=8):
return ''.join([str(randint(0, 9)) for i in range(length)])
Frontend GET и подписывает одноразовый номер с помощью web3
Получить публичный адрес текущего пользователя:
web3.eth.getAccounts()
.then((response) => {
const publicAddressResponse = response[0];
if (!(typeof publicAddressResponse === "undefined")) {
setPublicAddress(publicAddressResponse);
getNonce(publicAddressResponse);
}
})
.catch((e) => {
console.error(e);
});
Внешний интерфейс должен выполнить запрос GET, чтобы получить текущий одноразовый номер для публичного адреса, который пытается войти в систему. Если учетная запись еще не существует, создайте ее и все равно верните одноразовый номер:
GET /api/users?publicAddress=${publicAddress}
а затем подпишите одноразовый номер с помощью Metamask:
web3.eth.personal.sign(`I am signing my one-time nonce: ${nonce}`, publicAddress, "test password!")
.then((signature) => {
handleAuth(publicAddress, signature)
});
Затем внешний интерфейс отправляет подписанный одноразовый номер на серверную часть для получения JWT.
Внешний интерфейс:
axios.post(props.config.serverUrl + '/sessions/', {
publicAddress: publicAddress,
signature: signature,
auth_type: 'ethereum',
})
.then((response) => {
localStorage.setItem('accessToken', response.data.access_token);
})
.catch((e) => {
console.error(e);
});
Затем на серверной части вы аутентифицируете, что подпись пришла с этого общедоступного адреса, используя библиотеки web3, и выдаете JWT, если аутентифицированы. Оттуда это просто обычное управление сеансом с использованием JWT, что не является проблемой web3:
@sessions_blueprint.route('/sessions/', methods=['POST'])
def create_session():
auth_type = request.json.get('auth_type', AuthType.EMAIL)
public_address = request.json['publicAddress']
signature = request.json['signature']
account = EthereumAccount.query.filter_by(public_address=public_address).first()
if account is None:
abort(404, 'Public address not registered.')
original_message = 'I am signing my one-time nonce: {}'.format(account.nonce)
message_hash = defunct_hash_message(text=original_message)
signer = w3.eth.account.recoverHash(message_hash, signature=signature)
if signer == public_address:
account.nonce = account.generate_nonce()
db.session.commit()
else:
abort(401, 'could not authenticate signature')
access_token = create_access_token(identity=public_address)
refresh_token = create_refresh_token(identity=public_address)
return jsonify({
'access_token': access_token,
'refresh_token': refresh_token,
}), 200
И вуаля! Теперь пользователь аутентифицирован с помощью JWT на стороне клиента. Вы можете управлять сеансами и авторизацией маршрутов на бэкенде, как обычно.
Я узнал, как это сделать, из этой статьи: https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial
Я объясню в next.js для дальнейшего использования:
1- Сервер должен создать сообщение, создать сеанс, и клиент запросит это и сохранит в файлах cookie. Использование пакета iron-session npm упрощает создание сеанса, а также сохранение файла cookie в next.js.
if (req.method === "GET") {
try {
// message can be anything. I use id as password
const message = { contractAddress, id: uuidv4() };
req.session.messageSession = {
...message,
};
await req.session.save();
return res.json(message);
} catch (error) {
res.status(422).send({ message: "Cannot generate a message" });
}
2- Пользователь сделает GET
запрос. Если вы создаете nft, вы должны отправить данные json и изображение, для обоих вам необходимо выполнить процесс проверки. Вот почему я пишу повторно используемую функцию для получения данных и их подписи:
const createSignedData = async () => {
const messageToSign = await axios.get("/api/verify");
const accounts = (await ethereum?.request({
method: "eth_requestAccounts",
})) as string[];
// account will be the signer of this message
const account = accounts[0];
// password is the third param as uuid
const signedData = await ethereum?.request({
method: "personal_sign",
params: [
JSON.stringify(messageToSign.data),
account,
messageToSign.data.id,
],
});
return { signedData, account };
};
3- После того, как пользователь создает подписанные данные, он отправляет POST
запрос на сервер:
const createNft = async () => {
try {
const { account, signedData } = await createSignedData();
await axios.post("/api/verify", {
address: account,
signature: signedData,
nft: nftMeta,
})
} catch (error: any) {
console.log("error in createnft", error);
}
};
4- Сервер получит подписанные данные, теперь время проверки. для этого я пишу промежуточное ПО:
export const addressVerificationMiddleware = async (
req: NextApiRequest,
res: NextApiResponse
) => {
return new Promise(async (resolve, reject) => {
const message = req.session.messageSession;
// nonce is the representation of something that we are going to sign
let nonce: string | Buffer =
"\x19Ethereum Signed Message:\n" +
JSON.stringify(message).length +
JSON.stringify(message);
nonce = util.keccak(Buffer.from(nonce, "utf-8"));
const { v, r, s } = util.fromRpcSig(req.body.signature);
// matching signature with the unsigned message
const pubKey = util.ecrecover(util.toBuffer(nonce), v, r, s);
const addressBuffer = util.pubToAddress(pubKey);
const address = util.bufferToHex(addressBuffer);
if (address === req.body.address) {
resolve("Correct Address");
} else {
reject("Wrong Address");
}
});
};
Я создаю конечную точку и использую вышеуказанное промежуточное ПО.
if (req.method === "POST") {
try {
const { body } = req;
const nft = body.nft as NftMeta;
if (!nft.name || !nft.description || !nft.attributes) {
return res.status(422).send({ message: "Form data is missing" });
}
// addressCheckMiddleware
await addressVerificationMiddleware(req, res);
const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`;
const jsonResponse = await axios.post(
url,
{
pinataMetadata: {
name: uuidv4(),
},
pinataContent: nft,
},
{
headers: {
pinata_api_key: pinataApiKey,
pinata_secret_api_key: pinataSecretApiKey,
},
}
);
return res.status(200).send(jsonResponse.data);
} catch (error) {
console.error("error in verify post req", error);
res.status(422).send({ message: "Cannot create JSON" });
}
Флориан Пирчер
Дэйв Стейн
web3
больше не доступен на странице с использованием метамаски.