Skip to main content

Verifying signed in users on backend

This page describes a way for backend to ensure that the user truly owns the declared address. Please note that the user verification is not required for all DApps.

It is useful if you want to verify a user to provide them with their personal information from the back end.

How does it work?

  • User initiates sign in process.
  • Backend generates a ton_proof entity and sends it to frontend.
  • Frontend signs in to wallet using ton_proof and receives back a signed ton_proof.
  • Frontend sends signed ton_proof to backend for verification.


Structure of ton_proof

We will be using the TonProof entity, implemented inside connector.

type TonProofItemReply = TonProofItemReplySuccess | TonProofItemReplyError;

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
}
}

Checking ton_proof on Server Side

  1. Retrieve TonProofItemReply from a user.
  2. Verify that the received domain corresponds to the domain of your application.
  3. Check if TonProofItemReply.payload is permitted by original server and is still active.
  4. Check if timestamp is actual at the moment.
  5. Assemble a message according to the message scheme.
  6. Retrieve public_key either from API (a) or via back-end logic (b)
  • 6a:
    • Retrieve {public_key, address} from the walletStateInit with TON API method POST /v2/tonconnect/stateinit.
    • Check that the address extracted from walletStateInit corresponds to wallet address declared by user.
  • 6b:
    • Obtain the wallet public_key via the wallet contract get method.
    • If the contract is not active, or if it lacks the get_method found in older wallet versions (v1-v3), then obtaining the key in this manner will be impossible. Instead, you will need to parse the walletStateInit provided by the frontend. Ensure that TonAddressItemReply.walletStateInit.hash() is equal to TonAddressItemReply.address.hash(), indicating a BoC hash.
  1. Verify that the signature from the frontend actually signs the assembled message and corresponds to the public_key of the address.

React Example

  1. Add 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>
)
}
  1. Implement authentication on backend:
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])
}

Concept Explanation

If TonProofItem is requested, wallet proves ownership of the selected account’s key. The signed message is bound to:

  • Unique prefix to separate messages from on-chain messages. (ton-connect)
  • Wallet address
  • App domain
  • Signing timestamp
  • App’s custom payload (where server may put its nonce, cookie id, expiration time)
message = utf8_encode("ton-proof-item-v2/") ++
Address ++
AppDomain ++
Timestamp ++
Payload

signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message)))

where:

  • Address is the wallet address encoded as a sequence:
  • workchain: 32-bit signed integer big endian;
  • hash: 256-bit unsigned integer big endian;
  • AppDomain is Length ++ EncodedDomainName
  • Length is 32-bit value of utf-8 encoded app domain name length in bytes
  • EncodedDomainName id Length-byte utf-8 encoded app domain name
  • Timestamp 64-bit unix epoch time of the signing operation
  • Payload is a variable-length binary string.

Note: payload is variable-length untrusted data. We put it last to avoid using unnecessary length prefixes.

The signature must be verified by public key:

  1. First, try to obtain public key via get_public_key get-method on smart contract deployed at Address.

  2. If the smart contract is not deployed yet, or the get-method is missing, then:

    1. Parse TonAddressItemReply.walletStateInit and get public key from stateInit. You can compare the walletStateInit.code with the code of standard wallets contracts and parse the data according to the found wallet version.

    2. Check that TonAddressItemReply.publicKey equals to obtained public key

    3. Check that TonAddressItemReply.walletStateInit.hash() equals to TonAddressItemReply.address. .hash() means BoC hash.

Examples of TON Proof Verification

See Also