Signed-in user verification
This tutorial provides a step-by-step guide for backend developers to verify user identities using ton_proof
. Implementing this verification process ensures that a user genuinely owns the wallet address they claim, which is crucial for delivering personalized or protected data.
We will focus on the server-side logic required to validate the ton_proof
payload sent from the client.
Verification flow
The verification process begins after a user connects their wallet and the client-side application receives a ton_proof
object. This object is then sent to your backend for validation.
The backend must perform a series of checks to confirm the authenticity of the proof.
Step 1: Deconstruct the ton_proof payload
Upon receiving the TonProofItemReplySuccess
object from the client, the first action is to parse its structure. This object contains the proof of ownership that your server must validate.
// Received from the client
type TonProofItemReplySuccess = {
name: "ton_proof";
proof: {
timestamp: string; // 64-bit Unix epoch time of the signing operation (seconds)
domain: {
lengthBytes: number; // AppDomain length
value: string; // App domain name (as URL part, without encoding)
};
signature: string; // base64-encoded signature
payload: string; // Payload from the request
}
}
This structure provides the necessary components for verification: a timestamp to prevent replay attacks, the domain of the application, the signature to verify, and a payload which may contain a nonce.
Step 2: Verify the signature and message
The core of the verification process is to reconstruct the message that was signed by the wallet and then verify the provided signature against it.
Message assembly
The message signed by the wallet follows a strict format. Your backend must assemble an identical message to reproduce the hash that was signed.
The message is a concatenation of several fields:
message = utf8_encode("ton-proof-item-v2/") ++
Address ++
AppDomain ++
Timestamp ++
Payload
Address
: The user's wallet address, comprising aworkchain
(32-bit big-endian integer) and ahash
(256-bit big-endian integer).AppDomain
: The domain of your application, prefixed with its length.lengthBytes
is a 32-bit little-endian integer (number of bytes).Timestamp
: A 64-bit little-endian integer containing Unix epoch time in seconds.Payload
: A unique string, often a nonce, to prevent replay attacks.
Signature verification
The signature is an Ed25519 signature over sha256(0xffff ++ utf8_encode('ton-connect') ++ sha256(message))
:
signature = Ed25519Sign(
privkey,
sha256(
0xffff
++ utf8_encode("ton-connect")
++ sha256(message)))
Your backend must perform these steps in reverse: hash the message, prepend the prefixes, hash the result, and then use the user's public key to verify the signature.
Step 3: Obtain and verify the public key
A critical component for signature verification is the user's public key. There are two methods to obtain it, with a clear order of preference to ensure efficiency and security.
Primary method: Extract from walletStateInit
The recommended approach is to extract the public key directly from the walletStateInit
provided in the TonAddressItemReply
. This avoids a network call to the blockchain.
- Parse
walletStateInit
: Decode thewalletStateInit
cell. - Identify Wallet Version: Compare the
code
hash of thewalletStateInit
with known standard wallet contract codes to determine the wallet version. - Extract Public Key: Parse the
data
section of thewalletStateInit
according to the identified wallet version to retrieve the public key.
Fallback method: On-chain get_public_key
If the public key cannot be extracted from walletStateInit
(e.g., for older or non-standard wallets), the fallback is to query the blockchain.
- Call get method: Invoke the
get_public_key
get method on the smart contract at the user's address. - Extract Public Key: The result of this call will be the public key.
Final checks
Regardless of the method used, two final checks are mandatory:
- Public Key Consistency: The extracted public key must match the
publicKey
field in theTonAddressItemReply
. - Address Integrity: Derive the address as
contractAddress(workchain, walletStateInit)
and ensure it equals the user's address.
Note: The helper used in the example to extract a public key from stateInit
is simplified; replace it with version‑aware parsing after determining the wallet type (for example, via code hash) as described above.