Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Verifying Requests – ERC-8128
Skip to content

Verifying Requests

Learn how to verify ERC-8128 signed requests on your server.

Overview

Verification confirms that a request was signed by the claimed Ethereum address and hasn't been tampered with. The process checks:

  • Signature validity (cryptographic verification)
  • Timing (created/expires window)
  • Replay protection (nonce consumption)
  • Request binding (signed components match)

Basic Verification

The simplest way to verify a request using verifyRequest:

import { verifyRequest } from '@slicekit/erc8128'
 
const result = await verifyRequest({
  request,
  verifyMessage,
  nonceStore
})
 
if (result.ok) {
  console.log(`Authenticated: ${result.address}`)
  console.log(`Chain: ${result.chainId}`)
} else {
  console.log(`Failed: ${result.reason}`)
}

Using createVerifierClient

For repeated verification with the same dependencies, use createVerifierClient to bind verifyMessage and nonceStore:

import { createVerifierClient } from '@slicekit/erc8128'
 
const verifier = createVerifierClient({
  verifyMessage,
  nonceStore,
  defaults: {
  maxValiditySec: 120,
  },
})
 
const result = await verifier.verifyRequest({
  request,
  policy: { strictLabel: true },
})

Required Setup

Nonce Store

For replay protection, you need a store that tracks consumed nonces.

Redis (Production)
import type { NonceStore } from '@slicekit/erc8128'
import { Redis } from 'ioredis'
 
const redis = new Redis()
 
export const nonceStore: NonceStore = {
  async consume(key: string, ttlSeconds: number) {
    const result = await redis.set(`nonce:${key}`, '1', 'EX', ttlSeconds, 'NX')
    return result === 'OK'
  },
}

Policy Configuration

Choose the appropriate policy based on your endpoint's security requirements:

Strict (Mutations)
// For high-security endpoints that modify state
const result = await verifyRequest({
  request,
  verifyMessage: verifyMessageFn,
  nonceStore,
  policy: {
  replayable: false,
  maxValiditySec: 60,       // Short window
  maxNonceWindowSec: 60,
  clockSkewSec: 5,          // Small tolerance
  },
})

Label Selection

When multiple signatures exist on a request:

Default (Prefer Label)
// Prefer "eth" label, fall back to first valid
const result = await verifyRequest({
  request,
  verifyMessage: verifyMessageFn,
  nonceStore,
  policy: {
  label: 'eth',
  },
})

Component Policies

By default, the verifier enforces that signatures are request-bound — covering @authority, @method, @path, plus @query and content-digest when the request has a query string or body. This rejects all class-bound signatures. Non-replayable request-bound signatures always pass when their components match the required policy. Other cases depend on the configured policies and signature params (e.g. replayable, class-bound policy, timing, nonce window).

Use additionalRequestBoundComponents to require additional components alongside the default request-bound set. Use classBoundPolicies to opt into class-bound signatures by listing acceptable component sets (order does not matter):

// Default: rejects class-bound signatures (full request-bound check)
const result = await verifyRequest({
  request,
  verifyMessage: verifyMessageFn,
  nonceStore
})
 
// Require custom headers in request-bound signatures
const result = await verifyRequest({
  request,
  verifyMessage: verifyMessageFn,
  nonceStore,
  policy: {
  additionalRequestBoundComponents: ['x-idempotency-key'],
  },
})
 
// Accept class-bound signatures that cover a minimal policy
const result = await verifyRequest({
  request,
  verifyMessage: verifyMessageFn,
  nonceStore,
  policy: {
  classBoundPolicies: ['@authority', '@method'],
  },
})

See Server Enforcement for details on default behavior, custom components, and the difference between request-bound and class-bound verification.

Signature Selection Order

When a request contains multiple signatures (multiple labels in the headers), the verifier:

  1. Filters to ERC-8128 keyIds
  2. Filters to signatures allowed by your request-bound/class-bound policies
  3. Verifies the remaining candidates in the exact order they appear in Signature-Input

The first signature that passes all checks (cryptographic verification, time bounds, nonce, component policy) is accepted.

Framework Integration

Drop verifyRequest into middleware or route handlers. These examples show integration patterns for Hono, Next.js, and Express.

Hono
import { verifyRequest } from '@slicekit/erc8128'
import { createMiddleware } from 'hono/factory'
 
const erc8128Auth = createMiddleware(async (c, next) => {
  const result = await verifyRequest({
    request: c.req.raw,
    verifyMessage: verifyMessageFn,
    nonceStore,
    policy: {
      maxValiditySec: 300,
    },
  })
 
  if (!result.ok) {
    return c.json({ error: 'Unauthorized', reason: result.reason }, 401)
  }
 
  c.set('auth', {
    address: result.address,
    chainId: result.chainId,
  })
 
  await next()
})

Handling Failures

When verification fails, inspect result.reason to return appropriate HTTP status codes and error messages.

const result = await verifyRequest({
  request,
  verifyMessage: verifyMessageFn,
  nonceStore,
  policy,
})
 
if (!result.ok) {
  switch (result.reason) {
    case 'missing_headers':
      // No signature present
      return { status: 401, error: 'Authentication required' }
      
    case 'expired':
    case 'not_yet_valid':
    case 'validity_too_long':
      // Time-based rejection
      return { status: 401, error: 'Signature expired or not yet valid' }
      
    case 'replay':
    case 'replayable_not_allowed':
    case 'nonce_window_too_long':
      // Replay protection rejection
      return { status: 401, error: 'Request replay detected or not allowed' }
      
    case 'bad_signature':
    case 'bad_signature_bytes':
    case 'bad_signature_check':
      // Cryptographic verification failed
      return { status: 401, error: 'Invalid signature' }
      
    case 'digest_mismatch':
    case 'digest_required':
      // Body integrity issue
      return { status: 400, error: 'Body integrity check failed' }
      
    case 'not_request_bound':
    case 'class_bound_not_allowed':
      // Policy rejection
      return { status: 401, error: 'Signature does not meet security requirements' }
      
    default:
      return { status: 401, error: `Authentication failed: ${result.reason}` }
  }
}

Multi-Chain Support

The chainId in the verification result tells you which chain the signer claims to be on:

if (result.ok) {
  const { address, chainId } = result
 
  // Verify the signer has permissions on this chain
  const hasAccess = await checkPermissions(address, chainId)
 
  if (!hasAccess) {
    // Return 403 Forbidden
  }
}

Best Practices

  1. Always use nonce stores in production — In-memory stores don't work across server restarts or multiple instances.

  2. Keep validity windows short — 60-300 seconds is reasonable. Longer windows increase replay risk.

  3. Use strict label matching when you expect a specific signature source.

  4. Log verification failures — They can indicate attacks or client bugs.

  5. Consider clock skew — Allow 5-30 seconds for network latency and clock drift.

  6. Use Redis or a distributed store — Essential for multi-instance deployments.