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
- Retrieve
TonProofItemReply
from a user. - Verify that the received domain corresponds to the domain of your application.
- Check if
TonProofItemReply.payload
is permitted by original server and is still active. - Check if
timestamp
is actual at the moment. - Assemble a message according to the message scheme.
- Retrieve
public_key
either from API (a) or via back-end logic (b)
- 6a:
- Retrieve
{public_key, address}
from thewalletStateInit
with TON API methodPOST /v2/tonconnect/stateinit
. - Check that the
address
extracted fromwalletStateInit
corresponds to walletaddress
declared by user.
- Retrieve
- 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.
- Obtain the wallet
- Verify that the
signature
from the frontend actually signs the assembled message and corresponds to thepublic_key
of the address.
React Example
- 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>
)
}
- Implement authentication on the frontend 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
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.
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 bytesEncodedDomainName
idLength
-byte utf-8 encoded app domain name
Timestamp
64-bit unix epoch time of the signing operationPayload
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:
First, try to obtain public key via
get_public_key
get-method on smart contract deployed atAddress
.If the smart contract is not deployed yet, or the get-method is missing, then:
Parse
TonAddressItemReply.walletStateInit
and get public key from stateInit. You can compare thewalletStateInit.code
with the code of standard wallets contracts and parse the data according to the found wallet version.Check that
TonAddressItemReply.publicKey
equals to obtained public keyCheck that
TonAddressItemReply.walletStateInit.hash()
equals toTonAddressItemReply.address
..hash()
means BoC hash.