Skip to main content

Authentication

SHIELD uses Sign-In with Ethereum (SIWE) for authentication. No passwords required—just your wallet.

SIWE Flow

┌──────────┐                    ┌──────────┐
│  Client  │                    │  Server  │
└────┬─────┘                    └────┬─────┘
     │                               │
     │  1. Request nonce             │
     │ ───────────────────────────────▶│
     │                               │
     │  2. Return nonce              │
     │ ◀──────────────────────────────│
     │                               │
     │  3. Sign SIWE message         │
     │  (with nonce)                 │
     │                               │
     │  4. Send signature            │
     │ ───────────────────────────────▶│
     │                               │
     │  5. Verify signature          │
     │                               │
     │  6. Return JWT session        │
     │ ◀──────────────────────────────│
     │                               │
     │  7. Use JWT for API calls       │
     │ ───────────────────────────────▶│

Endpoints

1. Get Nonce

GET /api/signIn
Returns a unique nonce for signing. Response:
{
  "nonce": "1234567890abcdef"
}

2. Verify Signature

POST /api/verify-siwe
Content-Type: application/json
Request Body:
{
  "message": "shield.app wants you to sign in...",
  "signature": "0x..."
}
The message follows the EIP-4361 SIWE standard. Response:
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "address": "0x...",
  "expiresAt": "2025-02-23T10:00:00Z"
}

3. Authenticated Requests

Include the JWT in subsequent requests:
GET /api/user/links
Cookie: session=eyJhbGciOiJIUzI1NiIs...
Or via Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

SIWE Message Format

shield.app wants you to sign in with your Ethereum account:
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb

Sign this message to authenticate with SHIELD.

URI: https://app.shieldhq.xyz
Version: 1
Chain ID: 84532
Nonce: 1234567890abcdef
Issued At: 2025-02-22T10:00:00Z

Client Implementation

Using wagmi + siwe

import { SiweMessage } from 'siwe';
import { signMessage } from 'wagmi/actions';

async function authenticate(address: string, chainId: number) {
  // 1. Get nonce
  const nonceRes = await fetch('/api/signIn');
  const { nonce } = await nonceRes.json();

  // 2. Create SIWE message
  const message = new SiweMessage({
    domain: window.location.host,
    address,
    statement: 'Sign this message to authenticate with SHIELD.',
    uri: window.location.origin,
    version: '1',
    chainId,
    nonce,
  });

  // 3. Sign message
  const messageString = message.prepareMessage();
  const signature = await signMessage({ message: messageString });

  // 4. Verify
  const verifyRes = await fetch('/api/verify-siwe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message: messageString, signature }),
  });

  return await verifyRes.json();
}

Using ethers.js

import { SiweMessage } from 'siwe';
import { ethers } from 'ethers';

async function authenticate(signer: ethers.Signer) {
  const address = await signer.getAddress();
  const nonce = (await fetch('/api/signIn').then(r => r.json())).nonce;

  const message = new SiweMessage({
    domain: 'shield.app',
    address,
    statement: 'Sign this message to authenticate with SHIELD.',
    uri: 'https://app.shieldhq.xyz',
    version: '1',
    chainId: 84532,
    nonce,
  });

  const signature = await signer.signMessage(message.prepareMessage());

  const response = await fetch('/api/verify-siwe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: message.prepareMessage(),
      signature,
    }),
  });

  return await response.json();
}

Session Details

PropertyValue
Duration1 hour
StorageHTTP-only cookie
RenewalAutomatic on activity
RevocationOn logout or expiry

Security Considerations

Nonce Requirements

  • Must be used within 5 minutes
  • Single-use only
  • Cryptographically random (32 bytes)

Signature Verification

Server verifies:
  • Signature is valid
  • Address matches recovered signer
  • Nonce was issued by server
  • Domain matches expected
  • Chain ID is supported

Replay Protection

  • Nonces are single-use
  • Timestamps prevent old signatures
  • Sessions expire after 1 hour

Error Codes

CodeMeaning
INVALID_SIGNATURESignature doesn’t match address
EXPIRED_NONCENonce older than 5 minutes
USED_NONCENonce already consumed
DOMAIN_MISMATCHMessage domain doesn’t match
UNSUPPORTED_CHAINChain ID not supported

Testing

Test Authentication

# 1. Get nonce
curl https://app.shieldhq.xyz/api/signIn

# 2. Sign message (use your wallet)
# 3. Send to verify-siwe