Com a criptografia de chave pública subjacente ao Web3, um dos novos casos de uso que surgiram é a capacidade de verificar “quem você diz que é” sem a necessidade de um provedor de identidade centralizado. Embora existam muitas peças sendo construídas simultaneamente no espaço de identidade descentralizada em rápida evolução, a SpruceID está liderando a carga na frente de autenticação.
O SIWE (Sign In With Ethereum) (Entrar com Ethereum) rapidamente se estabeleceu como uma ferramenta essencial para usuários que exigem mais autonomia sobre sua própria identidade digital. Isso é obtido ao fornecer aos usuários a opção de autocustodiar sua própria identidade digital por meio de uma carteira compatível com EVM (ou seja, assinando uma mensagem verificável). Além disso, ao estabelecer um fluxo de trabalho de login padrão, o SIWE pode ser facilmente integrado aos serviços de identidade existentes e, portanto, não se limita apenas a aplicativos nativos Web3.
O SpruceID forneceu um exemplo de bloco de notas SIWE muito útil que implementa uma autenticação SIWE de ponta a ponta no Express.js, completa com gerenciamento de sessão.
O objetivo deste guia é focar apenas no fluxo de login principal usando a Metamask. Ao deixar de lado o gerenciamento de sessão, bem como a integração com outros provedores de carteira, espero simplificar os principais aspectos do SIWE:
- Autenticação SIWE no Cliente vs Servidor
- Construindo a mensagem de entrada para os usuários aprovarem
- Evitando ataques de repetição por meio da sincronização de número que pode ser usado uma única vez (nonce)
- Interagindo com o SIWE por meio da IU da Metamask
O repositório do Github para este guia pode ser encontrado aqui. Se você quiser uma introdução à interação programática com o Metamask:
- Conectando o Metamask com uma Rede Local de _Hardhat
- Conectando o Metamask ao seu aplicativo da Web (Express)
- Conectar Metamask com
Ethers.js
Sumário
1 . Configurando um esqueleto de aplicativo
2 . Criando uma autenticação com o botão SIWE
3 . Usando o SIWE para entrar no navegador
4 . Atualizando os nossos Endpoints do Servidor
5 . Fazendo login com interface do usuário Metamask
Configurando um esqueleto de aplicativo
Primeiro precisamos criar um diretório de projeto e instalar o Express:
mkdir MetamaskSIWE
cd MetamaskSIWE
npm install express
Com o pacote Express instalado, podemos utilizar o express-generator
para configurar um modelo de aplicativo de forma rápida e conveniente. Antes de executar o express-generator
, podemos visualizar as opções de configuração executando o seguinte comando:
npx express-generator --help
Para nossos propósitos, queremos inicializar nosso projeto usando EJS, pois isso minimiza a necessidade de troca de contexto devido às suas semelhanças com HTML. Assim, executaremos o express-generator
com o sinalizador de visualização EJS:
npx express-generator -v ejs
Conforme instruído, também instalaremos as dependências necessárias:
npm install
Por uma questão de conveniência, usaremos o nodemon para atualizar automaticamente nosso aplicativo sempre que fizermos alterações. O comando abaixo instalará o nodemon
globalmente para que você possa usá-lo em seus outros projetos:
npm install -g nodemon
Uma vez instalado, podemos iniciar nosso aplicativo da web executando o seguinte comando:
nodemon start
Você poderá ver uma página de boas-vindas abaixo ao visitar a porta padrão em localhost:3000
.
O Express agora está configurado e podemos prosseguir e fazer as alterações necessárias para conectar o Metamask ao nosso exemplo do Express.
Criando uma autenticação com o botão SIWE
A primeira coisa que iremos fazer é alterar a visualização para que a página exibida seja mais intuitiva para a nossa proposta. Navegue para dentro do arquivo index.ejs
localizado na pasta \views
. Nós podemos alterar o <body>
com o código abaixo:
<body>
<h1><%= title %></h1>
<button id="login">Login with SIWE</button>
<script src="javascripts/bundle.js"></script>
</body>
De acordo com as boas práticas do Metamask, incluímos um botão para o usuário inicializar a requisição de conexão. A requisição de conexão deve sempre ser inicializada pelo usuário e não no carregamento da página.
Adicionalmente, incluimos um script bundle.js
no qual estaremos criando brevemente. Esse script carregará o código requerido pelo Metamask para se conectar na aplicação.
Por último, também alterarmos o título que é inserido na página, modificando o index.js
dentro da pasta /routes
. O res.render()
alimenta os dados passados na função para o nosso arquivo index.ejs
a ser renderizado.
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'SIWE Example' });
});
module.exports = router;
Salvando o código acima, o nodemon
teria atualizado automaticamente seu aplicativo e seu navegador deveria exibir o seguinte:
Usando o SIWE para entrar no navegador
Para utilizar a biblioteca SIWE, primeiro vamos instalá-la através do código:
npm install siwe
Observe que o SIWE vem com o Ethers.js em suas dependências e, portanto, não há necessidade de instalar o Ethers.js novamente, caso contrário, haverá um conflito.
Como precisaremos executar este script no navegador, também usaremos o “Browserify]()https://browserify.org/ para solicitar
os módulos no navegador do cliente. Consulte a seção Browserify do guia anterior para obter mais detalhes sobre isso.
Consequentemente, primeiro criaremos um arquivo de entrada, provider.js
, que contém toda a lógica necessária para este guia. Esse arquivo será então “browserificado” com o arquivo de saída, bundle.js
, sendo colocado no diretório /public/javascripts/
.
touch provider.js
Podemos então copiar e colar o seguinte código em nosso arquivo provider.js
:
const { ethers } = require('ethers');
const { SiweMessage } = require('siwe');
const loginButton = document.querySelector('#login');
loginButton.addEventListener('click', async () => {
/**
* Obtenha o provedor e o signatário na janela do navegador
*/
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
/**
* Obtenha a conta ativa
*/
const [address] = await provider.listAccounts();
console.log(`Address used to construct SIWE message: ${address}`);
/**
* Obtenha um nonce gerado aleatoriamente pela biblioteca SIWE. Este nonce
* é adicionado na seção onde podemos verificá-lo ao entrar. Por questões de
* segurança, isso é gerado no servidor.
*/
const nonce = await fetch('/api/nonce').then(res => res.text());
console.log(`Nonce returned from server stored on client: ${nonce}`);
/**
* Obtenha o id da cadeia
*/
const chainId = (await provider.getNetwork()).chainId;
console.debug(chainId);
/**
* Cria a mensagem para o objeto.
*/
const message = new SiweMessage({
domain: document.location.host,
address,
chainId,
uri: document.location.origin,
version: '1',
statement: 'Metamask SIWE Example',
nonce,
});
console.log(`SIWE message constructed in the client:`)
console.debug(message);
/**
* Gera a mensagem a ser assinada e usa o provedor para pedir uma assinatura
*/
const signature = await signer.signMessage(message.prepareMessage());
console.log(`Signed message signature: ${signature}`);
/**
* Chama o endpoint sign_in para validar a mensagem. Se for válida, o console
* exibirá a mensagem do servidor.
*/
await fetch('/api/sign_in', {
method: 'POST',
body: JSON.stringify({message, signature}),
headers: { 'Content-Type': 'application/json' }
}).then(async (res) => {
const message = await res.text();
console.log(JSON.parse(message).message);
})
})
Primeiro instanciamos nosso botão usando o seletor de consulta e adicionamos um ouvinte de evento de clique no botão. Uma vez clicado, algumas coisas acontecerão:
Extraia o
provider
e osigner
da janela do navegador usando Ethers.js. Isso nos permite interagir programaticamente com nossa instância Metamask.Obtenha a conta Metamask ativa. Este será o endereço vinculado à solicitação de entrada.
Gere um nonce aleatório que será usado para validar o login entre o servidor e o cliente. Observe que esta NÃO é a conta nem o nonce de consenso usado no Ethereum. Para evitar que o nonce seja manipulado, ele é gerado no servidor chamando o endpoint
/api/nonce
.
Crie uma nova mensagem SIWE que nos permitirá chamar os métodos da biblioteca SIWE na mensagem. Você pode encontrar o código aqui.
Solicite ao usuário que assine a mensagem SIWE preparada, que gerará um recibo contendo a assinatura bruta da mensagem. Você pode consultar a função
signMessage()
do Ethers.js aqui.Chame o endpoint
/api/sign_in
para que o servidor valide e manipule o login. De acordo com as práticas recomendadas de gerenciamento de sessão, usaremos o método POST aqui para que informações confidenciais sejam incorporadas ao corpo da solicitação.
Salve o arquivo provider.js
e podemos executar o comando Browserify:
browserify providers.js -o public/javascripts/bundle.js
Um arquivo bundle.js
será criado no diretório público a partir do qual os recursos estáticos são servidos. Observe que nosso arquivo index.ejs
também contém uma tag de script que faz referência a esse caminho. Embora o nodemon
tenha atualizado automaticamente nosso aplicativo, ainda precisamos implementar /api/nonce
e /api/sign_in
antes que nosso login esteja totalmente funcional.
Atualizando os nossos Endpoints do Servidor
Agora vamos mudar para nosso arquivo index.js
localizado no diretório routes/
para adicionar nossos 2 endpoints.
O terminal /api/nonce
utilizará a função generateNonce()
da biblioteca SIWE, que gera um nonce suficientemente aleatório para evitar ataques de repetição. O código para generateNonce()
pode ser encontrado aqui. Nosso endpoint responderá com o nonce gerado:
const { SiweMessage, generateNonce } = require('siwe');
// Armazenando o nonce para checar novamente no login.
let nonce;
// Gerando o nonce
router.get('/api/nonce', async (req, res) => {
nonce = generateNonce();
console.log(`Nonce generated on server: ${nonce}`);
res.send(nonce);
})
Observe que também inicializamos o nonce
no nível do arquivo, em vez de salvá-lo em uma sessão para fins de legibilidade. O nonce
é armazenado no servidor para poder compará-lo com o nonce
retornado do cliente quando um usuário faz login
.
A maior parte do código de login será tratada pelo endpoint /api/sign_in
:
// Manipula a sessão no servidor Express
router.post('/api/sign_in', async (req, res) => {
/**
Obtenha a mensagem e a assinatura da requisição
*/
const { message, signature } = req.body;
/**
* Formata a mensagem
*/
const messageSIWE = new SiweMessage(message);
/**
* Cria uma instância do provedor padrão
*/
const provider = ethers.getDefaultProvider();
/**
* Valida a mensagem SIWE vinda do cliente
*/
const fields = await messageSIWE.validate(signature, provider); // pacote npm siwe para implementar a verificação
if (fields.nonce !== nonce) {
res.status(422).json({
message: "Invalid nonce: Client and Server nonce mismatch"
});
return;
}
console.log(`SIWE message `)
console.debug(fields);
console.log(`Successfully logged in on the server`)
/**
* Retorna a mensagem de sucesso para o cliente
*/
res.status(200).json({
message: "Successfully logged in!"
})
}
De acordo com nosso código de cliente, esse endpoint é implementado como uma solicitação POST para minimizar vazamentos de dados (isso terá que ser emparelhado com HTTPS para melhor segurança). Consequentemente, primeiro precisamos desestruturar a solicitação para obter a mensagem
e a assinatura
. A mensagem
é então formatada no equivalente do SiweMessage
. Esta SiweMessage
deve conter as mesmas informações da gerada no navegador.
Para validar o par de assinatura
e mensagem
, também exigimos que uma nova instância do provedor
seja criada. Nesse caso, usaremos o provedor padrão Ethers.js, pois precisamos apenas de uma maneira rápida de testar a função de entrada.
O login do usuário é então validado usando o método validate()
no equivalente do SiweMessage. A implementação mais recente pode ser encontrada no Github da SIWE. Observe que a validate()
será obsoleta em breve em favor da verify()
, que implementa proteções adicionais. No entanto, como esta última alteração ainda não foi implementada no NPM, este guia ainda usará a função validate()
.
Após a validação, os resultados são gravados nos campos. De maneira crítica, nosso código também implementa uma verificação para garantir que fields.nonce
seja equivalente ao nonce
que foi gerado antes da chamada do /api/nonce
. Depois que essa verificação for concluída, o usuário fez o login com êxito e podemos lidar com a sessão de acordo. Além disso, uma resposta de sucesso também é retornada ao cliente, que será registrada no console do navegador.
Fazendo login com interface do usuário Metamask
Agora podemos navegar em nosso navegador para testar o login
por meio da interface do usuário. Clique no botão “Login with SIWE” e você deverá receber uma solicitação de assinatura da Metamask.
Observe que a mensagem definida em nosso provider.js
é exibida para o usuário revisar. Além disso, você também verá o console imprimindo detalhes relevantes, como o nonce
e o SiweMessage
.
Você também deve ver o nonce
equivalente sendo gerado e impresso nos logs do servidor.
Podemos então clicar no botão “Sign” para aprovar esta transação. Isso criará a mensagem assinada que é impressa no console do navegador.
Com essa assinatura, o endpoint /api/sign_in
também será acionado, resultando na impressão do seguinte no console do servidor:
Depois de concluído, a mensagem de sucesso retornada pelo servidor será refletida de acordo no console do navegador:
Parabéns, você implementou com sucesso o login com Ethereum usando Metamask!
Obrigado por ficar até o fim. Adoraria ouvir seus pensamentos/comentários, então deixe um comentário. Estou ativo no twitter @AwKaiShin se você quiser receber informações mais digeríveis sobre informações relacionadas à criptografia ou visite meu site pessoal se quiser meus serviços :)
Este artigo é uma tradução de Aw Kai Shin feita por Felipe Gueller. Você pode encontrar o artigo original aqui.
Latest comments (0)