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.principal.address}`)
console.log(`Chain: ${result.principal.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: { maxValiditySec: 60 },
})Required Setup
Nonce Store
For replay protection, you need a store that tracks consumed nonces.
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:
// 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 Correlation
RFC 9421 labels only correlate Signature-Input and Signature members. ERC-8128 candidates are selected by their signed tag and CAIP-10 keyid, never by label.
Component Policies
By default, the verifier enforces that signatures are request-bound — covering @scheme, @authority, @method, @path, and @query unconditionally. A received non-empty body also requires a covered, recomputed content-digest, and a received content-type is always covered. This rejects all class-bound signatures unless a route explicitly enables a weaker policy. Other cases depend on the configured replay, timing, and nonce policies.
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.
Set classBoundPolicies: [] to explicitly disable class-bound signatures. An
empty list is equivalent to leaving the field unset; it does not create an
@authority-only policy.
Signature Selection Order
When a request contains multiple signatures (multiple labels in the headers), the verifier:
- Filters to ERC-8128 keyIds
- Filters to signatures allowed by your request-bound/class-bound policies
- 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.
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.principal.address,
chainId: result.principal.chainId,
})
await next()
})Handling Failures
Use formatErc8128ProblemDetails so malformed requests, insufficient permissions, authentication failures, and temporary verifier outages receive their specified status codes.
import { formatErc8128ProblemDetails } from '@slicekit/erc8128'
const result = await verifyRequest({
request,
verifyMessage: verifyMessageFn,
nonceStore,
policy,
})
if (!result.ok) {
const problem = formatErc8128ProblemDetails(result)
return { status: problem.status, body: problem }
}Infrastructure may reject a request before ERC-8128 verification runs. For
example, the playground worker's body-size limit returns a generic RFC 9457
Problem Details response with status 413. Such responses intentionally do
not contain a VerifyFailReason; use parseErc8128ProblemDetails only for
ERC-8128 verification failures.
Multi-Chain Support
The principal identity identifies the authenticated delegation root or base-profile account:
if (result.ok) {
const { address, chainId } = result.principal
// Verify the signer has permissions on this chain
const hasAccess = await checkPermissions(address, chainId)
if (!hasAccess) {
// Return 403 Forbidden
}
}Best Practices
-
Always use nonce stores in production — In-memory stores don't work across server restarts or multiple instances.
-
Document a route maximum and keep signer windows short — a baseline route must accept windows up to 60 seconds, while signers should choose the shortest delivery-safe window. Longer windows increase replay exposure and nonce-storage time.
-
Use strict label matching when you expect a specific signature source.
-
Log verification failures — They can indicate attacks or client bugs.
-
Consider clock skew — Allow 5-30 seconds for network latency and clock drift.
-
Use Redis or a distributed store — Essential for multi-instance deployments.