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).
- Defina o domain e a origem públicos que os usuários vão assinar (ex.: localhost / http://localhost:3000 no dev)
- Configure METAMASK_DOMAIN e METAMASK_URI exatamente iguais a esse host e origem
- Defina METAMASK_STATEMENT (texto no prompt da carteira)
- Defina METAMASK_CHAIN_IDS para cada chain aceita (ex.: 1 ou 1,11155111)
- Opcionalmente ajuste METAMASK_NONCE_TTL_SECONDS (padrão 300)
- 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.
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
- Cliente faz POST /api/v1/auth/metamask/nonce e recebe { nonce, expiresIn }
- Cliente conecta a carteira (eth_requestAccounts) e monta a mensagem EIP-4361 com domain, URI, statement, chainId e nonce
- Carteira assina com personal_sign; cliente faz POST { message, signature } em /api/v1/auth/metamask/verify
- 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.
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.
POST /api/v1/auth/metamask/verify
Content-Type: application/json
{
"message": "",
"signature": "0x..."
} {
"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.
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.
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
- Hub de autenticação social
- Guia Google OAuth2
- Guia Facebook OAuth2
- Guia X / Twitter OAuth2
- Guia GitHub OAuth2
- Guia Figma OAuth2
- Guia LinkedIn OpenID Connect
- Guia Slack OpenID Connect
- Guia Atlassian OAuth 2.0 (3LO)
- Guia GitLab OAuth2
- Guia Bitbucket OAuth2
- Guia Discord OAuth2
- Guia Twitch OAuth2
- Guia Steam OpenID 2.0
- Guia Amazon OAuth2
- Guia Patreon OAuth2
- Guia Dropbox OAuth2
- Guia Apple Sign In
- Guia Reddit OAuth2
- Auth & JWT