Authentication

Every write carries a wallet signature over the exact message string it submits. In sepolia mode, validators recover the signer from that signature and reject the write unless it matches the claimed wallet. In mock mode, the default, they check only that the signature is well-formed hex. The validator stores the signature next to the content, so downstream Oak mounts keep the attribution. The trust boundary survives the round-trip into AEM and lives below JCR.

The signing surface is the same across browser and mobile clients; what differs is wallet provider integration. Set up wallet connection first, then wire signing, then test one POST /v1/propose-write request with a real signature.

Overview

sequenceDiagram
    participant U as User
    participant MM as MetaMask
    participant App as Your App
    participant V as Validator

    U->>MM: Connect wallet
    MM->>App: Return address
    App->>MM: Sign content
    MM->>U: Approve signature
    U->>MM: Confirm
    MM->>App: Return signature
    App->>V: POST /v1/propose-write (message + signature + proposalId + txHash)
    V->>V: Verify signature (sepolia mode)
    V->>App: 202 Accepted, state PENDING
    App->>V: GET /v1/ops/operations/{proposalId}
    V->>App: state COMMITTED

Wallet Connection

MetaMask (Browser)

// Check if MetaMask is installed
if (typeof window.ethereum === 'undefined') {
  alert('Please install MetaMask');
  return;
}

// Request account access
const accounts = await window.ethereum.request({
  method: 'eth_requestAccounts'
});

const wallet = accounts[0];
console.log('Connected:', wallet);
// 0x742d35cc6634c0532925a3b844bc454e4438f44e

Mobile wallets

Any EIP-1193 provider works, for example the WalletConnect v2 Ethereum provider. The signing call below is the same; only the provider object changes. WalletConnect v1 and its bridge.walletconnect.org bridge are shut down.

Signing Content

Every write must be signed by your wallet to prove ownership.

What Gets Signed

Sign the exact message string you send with the write, using Ethereum personal_sign (EIP-191). The validator hashes "\x19Ethereum Signed Message:\n" + <UTF-8 byte length of message> + message with keccak-256 and recovers the signer. The message is also the content the validator stores, so any string works. JSON is common.

const message = JSON.stringify({ title: 'Hello!' });

For POST /v1/propose-delete, sign the exact contentPath you submit instead.

The signature does not cover the proposalId, the payment transaction, a nonce, or a timestamp, and the validator checks none of these against it.

Sign with MetaMask

async function signMessage(wallet, message) {
  return window.ethereum.request({
    method: 'personal_sign',
    params: [message, wallet],
  });
}

// Usage
const message = JSON.stringify({ title: 'Hello!' });
const signature = await signMessage(wallet, message);

Sign with ethers.js

import { ethers } from 'ethers';

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

const message = JSON.stringify({ title: 'Hello!' });

// signMessage is personal_sign (EIP-191)
const signature = await signer.signMessage(message);

Complete Write Flow

async function writeContent(organization, content, proposalId, txHash) {
  // 1. Get wallet
  const accounts = await window.ethereum.request({
    method: 'eth_requestAccounts'
  });
  const wallet = accounts[0];

  // 2. Sign the exact message you will submit
  const message = JSON.stringify(content);

  const signature = await window.ethereum.request({
    method: 'personal_sign',
    params: [message, wallet],
  });

  // 3. Submit to validator
  const response = await fetch('http://localhost:8090/v1/propose-write', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      // Only if the validator sets OAK_VALIDATOR_AUTH_TOKEN; no "Bearer " prefix
      // Authorization: OAK_VALIDATOR_AUTH_TOKEN,
    },
    body: new URLSearchParams({
      proposalId,
      walletAddress: wallet,
      organization,
      message,
      ethereumTxHash: txHash,
      signature,
    }),
  });
  
  // 202 Accepted: { state: 'PENDING', operationId, wallet, storagePath, ... }
  return response.json();
}

// proposalId: a unique 0x + 64 hex id. In sepolia mode you pay for it with the
// contract's settleWrite(proposalId, capabilityFlags) and pass that transaction's
// hash as txHash. In mock mode the oak-chain CLI reuses the proposalId as the txHash.
const proposalId = '0x' + [...crypto.getRandomValues(new Uint8Array(32))]
  .map((b) => b.toString(16).padStart(2, '0')).join('');

const result = await writeContent(
  'MyBrand',
  { title: 'Hello World!' },
  proposalId,
  proposalId
);

// 4. Poll until the write is COMMITTED, FAILED or TIMED_OUT. Ask the validator
// that accepted the write; operation records are not replicated.
const status = await fetch(
  `http://localhost:8090/v1/ops/operations/${result.operationId}`
).then((r) => r.json());

202 means queued, not stored. The write is COMMITTED only after a majority of validators have applied it.

Signature Verification

In sepolia mode, validators recover the signer address from the signature and the EIP-191 hash of the submitted message (ecrecover), then compare it to walletAddress without regard to case. A mismatch returns 401. In mock mode, the default, they check only that the signature is 0x followed by hex. The implementation is EthereumSignatureVerifier in oak-segment-consensus.

Authorization Rules

Data Table
Action
Requirement
Read
Validator token, if the operator sets one
Write
Validator token (if set) + wallet signature + payment (simulated in mock mode)
Delete
Validator token (if set) + wallet signature + payment + ownership

Path Ownership

The validator builds the storage path from your wallet address, so writes land under your wallet. Deletes outside it are rejected:

Allowed:  /oak-chain/74/2d/35/0xYOUR_WALLET/...
Rejected: /oak-chain/ab/cd/ef/0xOTHER_WALLET/...

Security Best Practices

  1. Never expose private keys - Use MetaMask or hardware wallets
  2. Verify domain - Check you're signing for the correct site
  3. Review content - Read what you're signing before confirming
  4. Do not rely on the signature for replay protection - The validator does not check nonces or timestamps

Next Steps