Prove Wallet Ownership
Cryptographically prove account ownership by signing data with a private key. Use the public address as an identifier and build authentication based on message signing.
Use JSON Web Tokens (JWT) to pass authenticated user identity.
Example uses:
- Authenticate sign-in: Prove account ownership.
- Authenticate actions: Authorize off-chain actions.
- Off-chain data: Display user-specific off-chain data.
How it works

Signing a message affirms control of the wallet address.
Four ingredients are required:
- User wallet address
- Private key
- Public key
- Message to sign
To verify ownership, provide a message for the user to sign. Validate the signature using the public key.
Client: Connect Wallet and Get Staking Address
The backend User model requires public address and nonce fields. The address must be unique.
On Cardano, use the wallet's staking address as the identifier. Retrieve it using wallet.getUsedAddresses().
Get the user's staking address and send it to the backend:
const { wallet, connected } = useWallet();
async function frontendStartLoginProcess() {
if (connected) {
const userAddress = (await wallet.getUsedAddresses())[0];
// do: send request with 'userAddress' to the backend
}
}Server: Generate Nonce and Store in Database
Generate a random nonce in the backend to create a unique authentication message. Use generateNonce() from Mesh.
Check the database for the userAddress. Create a new entry for new users. Store the new nonce for existing users.
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'
}Return the nonce for signing.
Client: Verify ownership by signing the nonce
Sign the nonce using the wallet's private key with wallet.signData(nonce, userAddress) (CIP-8).
Request authorization. The app processes the generated signature.
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
}
}Server: Verify Signature
The backend retrieves the user and nonce from the database.
Verify the signature using checkSignature.
If verified, issue a JWT or session identifier.
Prevent replay attacks by regenerating the nonce after verification.
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
}Putting It All Together
Frontend implementation:
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 implementation:
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
}
}This technique authenticates sign-ins or any user action.