MetaMask

MetaMask SIWE

Sign-In with Ethereum (EIP-4361) para carteiras web — emissão de nonce, verificação com personal_sign, JWTs e exemplos de frontend. Sem redirect OAuth.

Visão geral

O login MetaMask é opcional e controlado por METAMASK_AUTH_ENABLED. Quando desligado, os endpoints MetaMask respondem 503 Service Unavailable. Isto não é OAuth: a API implementa Sign-In with Ethereum (SIWE / EIP-4361). O cliente pede um nonce de uso único, a carteira assina uma mensagem estruturada com personal_sign, e POST /auth/metamask/verify devolve o mesmo formato de JWT do login por senha. Não há callback por redirect nem passo POST /auth/exchange.

  • • Ativado por feature flag no ambiente
  • • MetamaskSiweService verifica mensagens EIP-4361 (domain, URI, chainId, nonce, signature)
  • • Nonce armazenado no Redis com TTL de METAMASK_NONCE_TTL_SECONDS (padrão 300)
  • • Sem app no Developer Portal — domain / URI / statement / chain IDs devem bater com a mensagem do cliente
  • • E-mail sintético {address}@users.noreply.metamask.local — MetaMask não retorna e-mail
  • • Novos usuários recebem nome de exibição aleatório (adjetivo + substantivo)
  • • Mesmo formato de tokens do POST /auth/login

Setup MetaMask / SIWE

Não é necessário console OAuth externo. Alinhe METAMASK_DOMAIN e METAMASK_URI com o host e a origem embutidos na mensagem SIWE do cliente. Defina METAMASK_STATEMENT como o texto que o usuário assina, e METAMASK_CHAIN_IDS com os chain IDs permitidos (separados por vírgula, padrão 1).

  1. Defina o domain e a origem públicos que os usuários vão assinar (ex.: localhost / http://localhost:3000 no dev)
  2. Configure METAMASK_DOMAIN e METAMASK_URI exatamente iguais a esse host e origem
  3. Defina METAMASK_STATEMENT (texto no prompt da carteira)
  4. Defina METAMASK_CHAIN_IDS para cada chain aceita (ex.: 1 ou 1,11155111)
  5. Opcionalmente ajuste METAMASK_NONCE_TTL_SECONDS (padrão 300)
  6. Ative METAMASK_AUTH_ENABLED=true no .env

Variáveis de ambiente

Carregadas do .env via src/config/metamask-auth.config.ts (namespace metamaskAuth no ConfigService). Veja .env.example.

Environment.env
METAMASK_AUTH_ENABLED=true
METAMASK_DOMAIN=localhost
METAMASK_URI=http://localhost:3000
METAMASK_STATEMENT=Sign in to the NestJS Boilerplate
METAMASK_CHAIN_IDS=1
METAMASK_NONCE_TTL_SECONDS=300
METAMASK_OAUTH_DEFAULT_ROLES=

Roles padrão para novos usuários MetaMask

Novos usuários MetaMask recebem roles: [] por padrão (igual ao register). Defina METAMASK_OAUTH_DEFAULT_ROLES no .env como lista separada por vírgula (ex.: user ou user,manager). Usuários existentes vinculados por metamaskId mantêm as roles atuais.

Sequência

  1. Cliente faz POST /api/v1/auth/metamask/nonce e recebe { nonce, expiresIn }
  2. Cliente conecta a carteira (eth_requestAccounts) e monta a mensagem EIP-4361 com domain, URI, statement, chainId e nonce
  3. Carteira assina com personal_sign; cliente faz POST { message, signature } em /api/v1/auth/metamask/verify
  4. API verifica SIWE, faz upsert por metamaskId e devolve user + accessToken + refreshToken

Referência da API

1. Emitir nonce

Endpoint público. Cria um nonce de uso único no Redis (TTL METAMASK_NONCE_TTL_SECONDS, padrão 300). Retorna 503 quando o auth MetaMask está desligado.

HTTPPOST /api/v1/auth/metamask/nonce
POST /api/v1/auth/metamask/nonce
→ { "nonce": "...", "expiresIn": 300 }

503 feature desabilitada

2. Verificar assinatura SIWE

Endpoint público. Valida a mensagem EIP-4361 contra domain / URI / statement / chain IDs configurados, checa o nonce, recupera o endereço da assinatura, faz upsert do usuário e devolve JWTs (mesmo formato do login). Sem exchange code.

HTTP RequestPOST /api/v1/auth/metamask/verify
POST /api/v1/auth/metamask/verify
Content-Type: application/json

{
  "message": "",
  "signature": "0x..."
}
HTTP Response200 OK
{
  "user": {
    "id": "...",
    "email": "[email protected]",
    "firstName": "Fearful",
    "lastName": "Bear",
    "roles": [],
    "metamaskId": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
    "isActive": true
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

400 SIWE inválido / assinatura inválida / nonce expirado · 503 feature desabilitada

Frontend — Web (carteira)

Fluxo típico com MetaMask no browser: pedir contas, buscar nonce, montar a mensagem SIWE para que domain, URI, statement e chainId batam com a config do servidor, então personal_sign e chamar verify.

JavaScriptNonce + personal_sign + verify
const API = 'http://localhost:3000/api/v1';

const [{ nonce }] = await Promise.all([
  fetch(`${API}/auth/metamask/nonce`, { method: 'POST' }).then((r) => r.json()),
  window.ethereum.request({ method: 'eth_requestAccounts' }),
]);
const [address] = await window.ethereum.request({ method: 'eth_accounts' });
const message =
  `${window.location.host} wants you to sign in with your Ethereum account:\n` +
  `${address}\n\nSign in to the NestJS Boilerplate\n\n` +
  `URI: ${window.location.origin}\nVersion: 1\nChain ID: 1\nNonce: ${nonce}\n` +
  `Issued At: ${new Date().toISOString()}`;
const signature = await window.ethereum.request({
  method: 'personal_sign',
  params: [message, address],
});
const res = await fetch(`${API}/auth/metamask/verify`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message, signature }),
});
if (!res.ok) throw new Error(await res.text());
const { user, accessToken, refreshToken } = await res.json();
// persista os tokens como sua app exigir
sessionStorage.setItem('accessToken', accessToken);
// MetaMask não tem e-mail real — use metamaskId como identificador estável
console.log('signed in', user.metamaskId);
  • • METAMASK_DOMAIN deve bater com o host usado na mensagem SIWE (muitas vezes window.location.host)
  • • METAMASK_URI deve bater com a origem / campo URI da mensagem
  • • O chain ID da mensagem deve estar em METAMASK_CHAIN_IDS
  • • Armazene accessToken / refreshToken com segurança após o verify

Frontend — Mobile (WalletConnect / deep link)

No mobile, use um SDK de carteira (WalletConnect, MetaMask SDK, etc.) capaz de personal_sign uma mensagem EIP-4361. Chame os mesmos endpoints nonce e verify via HTTPS — não há allowlist de redirect OAuth.

React NativeVerify após assinatura da carteira
const API = 'https://api.example.com/api/v1';

async function loginWithMetamask(message, signature) {
  const res = await fetch(`${API}/auth/metamask/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message, signature }),
  });
  const data = await res.json();
  // data.accessToken, data.refreshToken, data.user
  return data;
}
  • • Aponte o app para a URL base da API em HTTPS
  • • Mantenha METAMASK_DOMAIN / METAMASK_URI alinhados aos valores da mensagem SIWE
  • • Identifique usuários por metamaskId — não pelo e-mail sintético

Notas de segurança

  • • SIWE amarra a assinatura a domain, URI, chainId e um nonce Redis de uso único
  • • Rejeite domain / URI / statement / chainId divergentes — não afrouxe checagens em produção
  • • MetaMask nunca fornece e-mail — endereço sintético é só para upsert interno
  • • Contas só-MetaMask têm password null — login / change-password falham até definir senha
  • • Prefira origens HTTPS em produção para URI e domain não serem spoofados facilmente

Modelo de usuário

Campos da entity / interface usados no login MetaMask:

  • • metamaskId — endereço Ethereum lowercase único nullable
  • • email — sintético {address}@users.noreply.metamask.local
  • • password — nullable para contas só-SIWE
  • • firstName / lastName — adjetivo + substantivo aleatórios no primeiro login
  • • isActive — true para novos usuários MetaMask
  • • roles — de METAMASK_OAUTH_DEFAULT_ROLES no env (padrão [])

Relacionados