Mobile
Mobile Wallet Adapter

Mobile Wallet Adapter

Dieser Inhalt wird übersetzt und wird hier verfügbar sein, sobald er fertig ist.

Signing Messages

Signing Messages

Not every interaction needs a transaction. Sometimes you just need to prove that a user controls a wallet, without writing anything to the blockchain or spending lamports.

Message signing enables authentication flows, off-chain attestations, and cryptographic proofs of ownership. Sign In With Solana (SIWS) standardizes this for web3 login, replacing email/password with wallet signatures.

Message signatures prove wallet ownership without touching the blockchain. They're free, instant, and verifiable by anyone.

In this lesson, you'll create a useSignMessage hook and learn how to implement Sign In With Solana for authentication.

How Message Signing Works

When you sign a transaction, you're authorizing a state change on Solana. When you sign a message, you're creating a cryptographic proof that says "this wallet approved this exact content."

The signature is deterministic: the same wallet signing the same message will always produce the same signature. Anyone with the public key can verify the signature was made by the corresponding private key, without ever seeing that private key.

text
Message + Private Key → Signature
Message + Signature + Public Key → True/False (verification)

This is Ed25519 cryptography, the same scheme Solana uses for transaction signatures.

Basic Message Signing

The signMessages method takes an array of payloads (byte arrays) and returns signatures:

typescript
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { toByteArray } from 'react-native-quick-base64';

async function signMessage(message: string): Promise<Uint8Array> {
  return await transact(async (wallet) => {
    const authResult = await wallet.authorize({
      identity: APP_IDENTITY,
      chain: 'solana:mainnet',
    });
    
    // Convert string to bytes
    const messageBytes = new TextEncoder().encode(message);
    
    // Sign the message
    const signatures = await wallet.signMessages({
      addresses: [authResult.accounts[0].address],
      payloads: [messageBytes],
    });
    
    // signatures[0] is Uint8Array (64 bytes)
    return signatures[0];
  });
}

Key points:

  • addresses: Which account(s) should sign

  • payloads: The raw bytes to sign

  • Returns: Array of signatures matching the payloads

The wallet shows users the message content (decoded as UTF-8 if possible) and asks for approval.

Signing Multiple Messages

You can sign multiple messages in one request:

typescript
const signatures = await wallet.signMessages({
  addresses: [account.address, account.address],
  payloads: [messageBytes1, messageBytes2],
});

// signatures[0] corresponds to messageBytes1
// signatures[1] corresponds to messageBytes2

Verifying Signatures

After receiving a signature, you (or your backend) can verify it:

typescript
import { sign } from 'tweetnacl';
import { PublicKey } from '@solana/web3.js';

function verifySignature(
  message: Uint8Array,
  signature: Uint8Array,
  publicKey: PublicKey
): boolean {
  return sign.detached.verify(
    message,
    signature,
    publicKey.toBytes()
  );
}

// Usage
const message = new TextEncoder().encode('Hello, Solana!');
const signature = await signMessage('Hello, Solana!');
const publicKey = /* user's public key from authorization */;

const isValid = verifySignature(message, signature, publicKey);
console.log('Signature valid:', isValid); // true

Important: You must verify against the exact same bytes that were signed. If you encode the message differently (different encoding, whitespace, newlines), verification will fail.

Sign In With Solana

Sign In With Solana (SIWS) is a standard for wallet-based authentication. It's like "Sign in with Google" but using your Solana wallet instead of an OAuth provider.

MWA 2.0 supports SIWS directly in the authorize method:

typescript
await transact(async (wallet) => {
  const authResult = await wallet.authorize({
    identity: APP_IDENTITY,
    chain: 'solana:mainnet',
    sign_in_payload: {
      domain: 'mydapp.com',
      statement: 'Sign in to My dApp',
      uri: 'https://mydapp.com',
      version: '1',
      nonce: generateSecureNonce(), // Server-generated
      issuedAt: new Date().toISOString(),
    },
  });
  
  if (authResult.sign_in_result) {
    const { address, signature, signedMessage } = authResult.sign_in_result;
    
    // Send to backend for verification
    await verifySignInOnServer(address, signature, signedMessage);
  }
});

Why SIWS?

  • No passwords: Users authenticate with their wallet

  • Cryptographic proof: Impossible to forge

  • Decentralized: No OAuth provider dependency

  • Cross-platform: Same wallet works everywhere

The SIWS Message Format

The sign_in_payload becomes a human-readable message shown to the user:

text
mydapp.com wants you to sign in with your Solana account:
7F9k...Xyz

Sign in to My dApp

URI: https://mydapp.com
Version: 1
Nonce: abc123
Issued At: 2024-01-15T10:30:00Z

This message is what the user signs. The wallet displays it clearly so users know what they're approving.

SIWS Payload Fields

typescript
type SignInPayload = {
  domain: string;      // Your domain (e.g., "mydapp.com")
  statement?: string;  // Human-readable description
  uri: string;         // Full URI (e.g., "https://mydapp.com")
  version: string;     // SIWS version, currently "1"
  nonce: string;       // Server-generated random string
  issuedAt: string;    // ISO 8601 timestamp
  expirationTime?: string;  // When this sign-in expires
  notBefore?: string;       // Don't accept before this time
  requestId?: string;       // Your session identifier
  resources?: string[];     // URIs the user is agreeing to access
};

The nonce is critical; it prevents replay attacks. Your server generates a unique nonce, the user signs it, and your server verifies the signature includes that exact nonce.

Backend Verification

Never trust client-side verification for authentication. Always verify SIWS signatures on your backend.

Node.js Example

typescript
import { verifySignIn } from '@solana/wallet-standard-util';
import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';

export function verifySIWS(
  input: SolanaSignInInput,
  output: SolanaSignInOutput
): boolean {
  // Convert to format expected by verifySignIn
  const serializedOutput = {
    account: {
      publicKey: new Uint8Array(output.account.publicKey),
      ...output.account,
    },
    signature: new Uint8Array(output.signature),
    signedMessage: new Uint8Array(output.signedMessage),
  };
  
  return verifySignIn(input, serializedOutput);
}

// Express route example
app.post('/api/auth/siws', async (req, res) => {
  const { input, output } = req.body;
  
  // Verify the nonce matches what we issued
  const expectedNonce = await redis.get(`siws:nonce:${req.sessionID}`);
  if (input.nonce !== expectedNonce) {
    return res.status(400).json({ error: 'Invalid nonce' });
  }
  
  // Verify the signature
  const isValid = verifySIWS(input, output);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Signature valid - create session
  const publicKey = output.account.address;
  req.session.wallet = publicKey;
  
  // Clear the used nonce
  await redis.del(`siws:nonce:${req.sessionID}`);
  
  res.json({ success: true, publicKey });
});

Nonce Generation

typescript
import crypto from 'crypto';

export function generateSecureNonce(): string {
  return crypto.randomBytes(32).toString('base64url');
}

// Store nonce before sending to client
app.get('/api/auth/nonce', async (req, res) => {
  const nonce = generateSecureNonce();
  await redis.setex(`siws:nonce:${req.sessionID}`, 300, nonce); // 5 min expiry
  res.json({ nonce });
});

SIWS Without Wallet Support

If a wallet doesn't support the sign_in_payload parameter, you can manually implement SIWS using signMessages:

typescript
import { createSignInMessage, SolanaSignInInput } from '@solana/wallet-standard-util';

async function manualSignIn(): Promise<{ input: SolanaSignInInput; output: any }> {
  // Fetch nonce from server
  const { nonce } = await fetch('/api/auth/nonce').then(r => r.json());
  
  const input: SolanaSignInInput = {
    domain: 'mydapp.com',
    statement: 'Sign in to My dApp',
    uri: 'https://mydapp.com',
    version: '1',
    nonce,
    issuedAt: new Date().toISOString(),
  };
  
  return await transact(async (wallet) => {
    const authResult = await wallet.authorize({
      identity: APP_IDENTITY,
      chain: 'solana:mainnet',
    });
    
    const account = authResult.accounts[0];
    const publicKey = new PublicKey(toByteArray(account.address));
    
    // Create the standard SIWS message
    const messageBytes = createSignInMessage({
      ...input,
      address: publicKey.toBase58(),
    });
    
    // Sign it
    const [signature] = await wallet.signMessages({
      addresses: [account.address],
      payloads: [messageBytes],
    });
    
    return {
      input,
      output: {
        account: {
          publicKey: publicKey.toBytes(),
          address: publicKey.toBase58(),
        },
        signature,
        signedMessage: messageBytes,
      },
    };
  });
}

Use Cases

Authentication

Replace username/password with wallet signatures:

typescript
async function login(): Promise<void> {
  const { input, output } = await signInWithSolana();
  
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ input, output }),
  });
  
  if (response.ok) {
    const { token } = await response.json();
    await AsyncStorage.setItem('auth_token', token);
  }
}

Off-Chain Attestations

Prove agreement to terms of service without a transaction:

typescript
const message = `I agree to the Terms of Service for MyApp.
Document Hash: ${tosHash}
Timestamp: ${Date.now()}`;

const signature = await signMessage(message);

// Store signature as proof of agreement
await fetch('/api/tos/agree', {
  method: 'POST',
  body: JSON.stringify({
    wallet: publicKey.toBase58(),
    signature: Buffer.from(signature).toString('base64'),
    message,
  }),
});

Gated Content Access

Prove NFT ownership without revealing the wallet's full holdings:

typescript
const message = `Verify NFT ownership for access:
Collection: ${collectionAddress}
Timestamp: ${Date.now()}`;

const signature = await signMessage(message);

// Backend verifies wallet holds an NFT from the collection
const { hasAccess } = await fetch('/api/verify-access', {
  method: 'POST',
  body: JSON.stringify({
    wallet: publicKey.toBase58(),
    signature: Buffer.from(signature).toString('base64'),
    message,
  }),
}).then(r => r.json());

Security Considerations

Always Use Nonces

Without a nonce, signatures can be replayed:

  1. User signs in to your app

  2. Attacker captures the signature

  3. Attacker replays it to impersonate the user

Nonces make each signature unique and single-use.

Domain Binding

The SIWS domain field should match your actual domain. Wallets may warn users if the domain looks suspicious.

Message Clarity

Users see the message before signing. Make it clear what they're approving:

typescript
// Bad: vague, could be anything
const message = 'confirm action';

// Good: specific, includes context
const message = `Sign in to MyApp
Request ID: ${requestId}
This does not grant access to your funds.`;

Timestamp Validation

Include and validate timestamps to prevent old signatures from being reused:

typescript
function isSignatureExpired(issuedAt: string, maxAgeMs: number = 300000): boolean {
  const issued = new Date(issuedAt).getTime();
  return Date.now() - issued > maxAgeMs;
}

Message signing is the foundation of wallet-based authentication. In the next lesson, we'll build an AuthorizationProvider to manage all this state across your application.

Blueshift © 2026Commit: 1b8118f