Mesh LogoMesh

How to Prove Wallet Ownership with Cardano Message Signing

Implement wallet authentication in your Cardano dApp development using CIP-8 message signing and the Mesh TypeScript blockchain SDK.

Wallet ownership verification lets users prove they control a Cardano address by signing a message with their private key. This is the foundation for "Sign in with Cardano" authentication in JavaScript Web3 applications—similar to "Sign in with Ethereum" but using CIP-8 message signing with the Mesh Cardano SDK.

Common use cases:

  • User authentication — Replace passwords with wallet-based sign-in
  • Action authorization — Verify user consent for off-chain operations
  • Access control — Gate content or features to specific wallet holders

How Does Wallet Authentication Work?

cryptographically-prove-wallet-ownership-process

The authentication flow requires four components:

  1. User wallet address — Unique identifier for the user
  2. Private key — Held securely in the user's wallet (never transmitted)
  3. Public key — Used to verify signatures
  4. Message (nonce) — Random string that prevents replay attacks

The server generates a unique nonce, the user signs it with their wallet, and the server verifies the signature matches the claimed address.

Step 1: How Do You Get the User's Wallet Address?

Use the wallet's staking address as the user identifier since it remains constant across transactions. Your backend User model needs address and nonce fields:

const { wallet, connected } = useWallet();

async function frontendStartLoginProcess() {
  if (connected) {
    const userAddress = (await wallet.getUsedAddresses())[0];

     // do: send request with 'userAddress' to the backend
  }
}

Step 2: How Do You Generate a Nonce on the Server?

The server generates a random nonce that the user must sign. This prevents replay attacks—each authentication attempt requires a fresh signature. Use generateNonce() from Mesh:

import { generateNonce } from '@meshsdk/core';

async function backendGetNonce(userAddress) {
  // do: if new user, create new user model in the database

  const nonce = generateNonce('I agree to the term and conditions of the Mesh: ');

  // do: store 'nonce' in user model in the database

  // do: return 'nonce'
}

Store the nonce in your database associated with the user's address. Create a new user record if this is their first sign-in attempt.

Step 3: How Does the Wallet Sign the Nonce?

The frontend prompts the user to sign the nonce using CIP-8 message signing. This proves they control the private key for the claimed address:

async function frontendSignMessage(nonce) {
  try {
    const userAddress = (await wallet.getUsedAddresses())[0];
    const signature = await wallet.signData(nonce, userAddress);

    // do: send request with 'signature' and 'userAddress' to the backend
  } catch (error) {
    // catch error if user refuse to sign
  }
}

The wallet prompts the user to approve the signature. Handle the case where users reject the signing request.

Step 4: How Do You Verify the Signature on the Server?

Retrieve the stored nonce from your database and verify the signature using checkSignature. If valid, issue a JWT or session token. Always regenerate the nonce after verification to prevent replay attacks:

import { checkSignature } from '@meshsdk/core';

async function backendVerifySignature(userAddress, signature) {
  // do: get 'nonce' from user (database) using 'userAddress'

  const result = checkSignature(nonce, signature, userAddress);

  // do: update 'nonce' in the database with another random string

  // do: do whatever you need to do, once the user has proven ownership
  // it could be creating a valid JSON Web Token (JWT) or session
  // it could be doing something offchain
  // it could just be updating something in the database
}

Complete Implementation Example

Here's a complete React blockchain component using Mesh's CardanoWallet for a "Sign in with Cardano" flow:

import { CardanoWallet, useWallet } from '@meshsdk/react';

export default function Page() {
  const { wallet, connected } = useWallet();

  async function frontendStartLoginProcess() {
    if (connected) {
      const userAddress = (await wallet.getUsedAddresses())[0];
      const nonce = await backendGetNonce(userAddress);
      await frontendSignMessage(nonce);
    }
  }

  async function frontendSignMessage(nonce) {
    try {
      const userAddress = (await wallet.getUsedAddresses())[0];
      const signature = await wallet.signData(nonce, userAddress);
      await backendVerifySignature(userAddress, signature);
    } catch (error) {
      setState(0);
    }
  }

  return (
    <>
      <CardanoWallet
        label="Sign In with Cardano"
        onConnected={() => frontendStartLoginProcess()}
      />
    </>
  );
}

Server-side functions:

import { checkSignature, generateNonce } from '@meshsdk/core';

async function backendGetNonce(userAddress) {
  const nonce = generateNonce('I agree to the term and conditions of the Mesh: ');
  return nonce;
}

async function backendVerifySignature(userAddress, signature) {
  // do: get 'nonce' from database

  const result = checkSignature(nonce, signature, userAddress);
  if(result){
    // create JWT or approve certain process
  }
  else{
    // prompt user that signature is not correct
  }
}

What Security Considerations Apply?

When building Cardano dApp development authentication flows, follow these security best practices:

  • Always regenerate nonces — A used nonce should never be valid again
  • Set nonce expiration — Implement a timeout (e.g., 5 minutes) for unused nonces
  • Use HTTPS — Protect the signature during transmission
  • Validate addresses — Ensure the address format is valid before database operations

On this page