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.Timestamp
: A 64-bit Unix timestamp.Payload
: A unique string, often a nonce, to prevent replay attacks.
Signature verification
The final signature is a hash of the assembled message, prefixed with 0xffff
and ton-connect
:
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: The hash of the
walletStateInit
must match the user'saddress
.
React example
- Add a token provider to the root of your app:
function App() {
const [token, setToken] = useState<string | null>(null);
return (
<BackendTokenContext.Provider value={{token, setToken}}>
{ /* Your app */ }
</BackendTokenContext.Provider>
)
}
- Implement authentication on the front end with backend integration:
Example
import {useContext, useEffect, useRef} from "react";
import {BackendTokenContext} from "./BackendTokenContext";
import {useIsConnectionRestored, useTonConnectUI, useTonWallet} from "@tonconnect/ui-react";
import {backendAuth} from "./backend-auth";
const localStorageKey = 'my-dapp-auth-token';
const payloadTTLMS = 1000 * 60 * 20;
export function useBackendAuth() {
const { setToken } = useContext(BackendTokenContext);
const isConnectionRestored = useIsConnectionRestored();
const wallet = useTonWallet();
const [tonConnectUI] = useTonConnectUI();
const interval = useRef<ReturnType<typeof setInterval> | undefined>();
useEffect(() => {
if (!isConnectionRestored || !setToken) {
return;
}
clearInterval(interval.current);
if (!wallet) {
localStorage.removeItem(localStorageKey);
setToken(null);
const refreshPayload = async () => {
tonConnectUI.setConnectRequestParameters({ state: 'loading' });
const value = await backendAuth.generatePayload();
if (!value) {
tonConnectUI.setConnectRequestParameters(null);
} else {
tonConnectUI.setConnectRequestParameters({state: 'ready', value});
}
}
refreshPayload();
setInterval(refreshPayload, payloadTTLMS);
return;
}
const token = localStorage.getItem(localStorageKey);
if (token) {
setToken(token);
return;
}
if (wallet.connectItems?.tonProof && !('error' in wallet.connectItems.tonProof)) {
backendAuth.checkProof(wallet.connectItems.tonProof.proof, wallet.account).then(result => {
if (result) {
setToken(result);
localStorage.setItem(localStorageKey, result);
} else {
alert('Please try another wallet');
tonConnectUI.disconnect();
}
})
} else {
alert('Please try another wallet');
tonConnectUI.disconnect();
}
}, [wallet, isConnectionRestored, setToken])
}
Backend example
The following example is adapted from the demo dApp with React UI and demonstrates a complete server-side verification implementation. For this code to function, your project must replicate a similar structure.
You can review our example showcasing the key methods:
- generatePayload: Generates a payload for
ton_proof
. - checkProof: Checks the proof and returns an access token.
Full backend verification example
import { Address, Cell, contractAddress, loadStateInit } from '@ton/core';
import { sign } from 'tweetnacl';
import { sha256 } from 'js-sha256';
// This DTO is simplified for demonstration.
// See the full structure in the demo dApp repository.
interface CheckProofRequestDto {
address: string;
public_key: string;
proof: {
timestamp: number;
domain: {
lengthBytes: number;
value: string;
};
signature: string;
payload: string;
state_init: string;
};
}
const tonProofPrefix = 'ton-proof-item-v2/';
const tonConnectPrefix = 'ton-connect';
const allowedDomains = ['YOUR_APP_DOMAIN.com']; // Replace with your app's domain
const validAuthTime = 600; // 10 minutes
// Attempts to extract the public key from a wallet's stateInit.
function tryParsePublicKey(stateInit: Cell): Buffer | null {
try {
const stateInitData = loadStateInit(stateInit.beginParse());
if (stateInitData.data) {
const reader = stateInitData.data.beginParse();
reader.skip(32); // seqno
return reader.loadBuffer(32); // public key
}
} catch (e) {
console.error("Failed to parse public key from stateInit", e);
}
return null;
}
// This function checks the proof and is based on the implementation
// in the official demo dApp.
export async function checkProof(payload: CheckProofRequestDto, getWalletPublicKey: (address: string) => Promise<Buffer | null>): Promise<boolean> {
try {
const stateInitCell = Cell.fromBase64(payload.proof.state_init);
const stateInit = loadStateInit(stateInitCell.beginParse());
// Obtain the public key from stateInit or via a get-method call.
let publicKey = tryParsePublicKey(stateInitCell) ?? await getWalletPublicKey(payload.address);
if (!publicKey) {
return false;
}
// Verify that the public key matches the one from the payload.
const wantedPublicKey = Buffer.from(payload.public_key, 'hex');
if (!publicKey.equals(wantedPublicKey)) {
return false;
}
// Verify that the wallet address matches the one derived from stateInit.
const wantedAddress = Address.parse(payload.address);
const address = contractAddress(wantedAddress.workChain, stateInit);
if (!address.equals(wantedAddress)) {
return false;
}
// Verify that the request originates from an allowed domain.
if (!allowedDomains.includes(payload.proof.domain.value)) {
return false;
}
// Verify that the proof's timestamp is recent.
const now = Math.floor(Date.now() / 1000);
if (now - validAuthTime > payload.proof.timestamp) {
return false;
}
const message = {
workchain: address.workChain,
address: address.hash,
domain: {
lengthBytes: payload.proof.domain.lengthBytes,
value: payload.proof.domain.value,
},
signature: Buffer.from(payload.proof.signature, 'base64'),
payload: payload.proof.payload,
stateInit: payload.proof.state_init,
timestamp: payload.proof.timestamp
};
// Reconstruct the message that was signed.
const wc = Buffer.alloc(4);
wc.writeUInt32BE(message.workchain, 0);
const ts = Buffer.alloc(8);
ts.writeBigUInt64LE(BigInt(message.timestamp), 0);
const dl = Buffer.alloc(4);
dl.writeUInt32LE(message.domain.lengthBytes, 0);
const msg = Buffer.concat([
Buffer.from(tonProofPrefix),
wc,
message.address,
dl,
Buffer.from(message.domain.value),
ts,
Buffer.from(message.payload),
]);
// Hash the message and verify the signature.
const msgHash = Buffer.from(sha256.create().update(msg).digest());
const fullMsg = Buffer.concat([
Buffer.from([0xff, 0xff]),
Buffer.from(tonConnectPrefix),
msgHash,
]);
const result = Buffer.from(sha256.create().update(fullMsg).digest());
return sign.detached.verify(result, message.signature, publicKey);
} catch (e) {
console.error(e);
return false;
}
}
Conclusion
This tutorial outlined the server-side process for ton_proof
verification. By following these steps, you can build a robust authentication system that securely validates user ownership of a TON wallet. The key takeaways are the importance of the specific message structure, the dual-method approach for public key retrieval, and the final signature verification.
For further development, you could extend this verification logic to manage user sessions, issue JWT tokens for authenticated API access, or integrate with more complex authorization schemes based on wallet addresses.