Skip to content

Frontend implementation guidelines

Julien Béranger edited this page Jul 31, 2024 · 1 revision

In a real application, the nonce plays a crucial role in preventing replay attacks and ensuring the freshness of each authentication request. Here's how it typically works with a frontend:

Initial Nonce Retrieval:

  • When a user wants to authenticate, the frontend should first query the current nonce for the user's address from the smart contract.
  • This can be done by calling the getNonce(userAddress) function.

Signature Creation:

  • The frontend uses this retrieved nonce to create the message that the user needs to sign.
  • The message includes the user's address and the current nonce.

User Signing:

  • The user signs this message using their wallet (e.g., MetaMask).

Sending Authentication Request:

  • The frontend sends the signed message along with the user's address to your backend or directly to the smart contract (via a relayer).

Nonce Increment:

  • If the authentication is successful, the smart contract automatically increments the nonce for that user.

Subsequent Authentication Attempts:

  • For any future authentication attempts, the frontend must always fetch the latest nonce before creating a new signature.

Here's a simplified example of how this might look in a React frontend:

import { ethers } from 'ethers';

interface AuthenticatorContract extends ethers.Contract {
  getNonce(address: string): Promise<ethers.BigNumber>;
}

interface AuthResult {
  success: boolean;
}

async function authenticate(): Promise<void> {
  try {
    // Connect to the user's wallet (e.g., MetaMask)
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    const signer = provider.getSigner();
    const userAddress: string = await signer.getAddress();

    // Get the current nonce from the smart contract
    const authenticatorContract: AuthenticatorContract = new ethers.Contract(
      authenticatorContractAddress,
      authenticatorABI,
      provider
    ) as AuthenticatorContract;
    const nonce: ethers.BigNumber = await authenticatorContract.getNonce(userAddress);

    // Prepare the message to be signed
    const domain: ethers.TypedDataDomain = {
      name: "Authenticator",
      version: "1",
      chainId: (await provider.getNetwork()).chainId,
      verifyingContract: authenticatorContractAddress
    };

    const types: Record<string, Array<ethers.TypedDataField>> = {
      Authenticate: [
        { name: "user", type: "address" },
        { name: "nonce", type: "uint256" }
      ]
    };

    const value: Record<string, any> = {
      user: userAddress,
      nonce: nonce.toNumber()
    };

    // Ask the user to sign the message
    const signature: string = await signer._signTypedData(domain, types, value);

    // Send the signature to your backend or directly to the contract
    const result: AuthResult = await sendToBackendOrContract(userAddress, signature);

    // Handle the authentication result
    if (result.success) {
      console.log("Authentication successful");
    } else {
      console.log("Authentication failed");
    }
  } catch (error) {
    console.error("Authentication error:", error);
  }
}

// This function needs to be implemented based on your backend or contract interaction
async function sendToBackendOrContract(userAddress: string, signature: string): Promise<AuthResult> {
  // Implementation depends on your specific backend or contract interaction
  // This is just a placeholder
  return { success: true };
}

// These would typically be defined elsewhere in your application
const authenticatorContractAddress: string = "0x..."; // Your contract address
const authenticatorABI: ethers.ContractInterface = []; // Your contract ABI

Key points to remember:

  • Always fetch the latest nonce before creating a signature.
  • The nonce in the frontend should always match the nonce in the smart contract for a successful authentication.
  • If an authentication attempt fails due to an outdated nonce, you should fetch the latest nonce and try again.
  • Consider implementing error handling for scenarios where the nonce might be out of sync.
  • In a production environment, you might want to implement a caching mechanism or state management to reduce the number of calls to the blockchain for nonce retrieval.

By following these practices, you ensure that each authentication request is unique and tied to the current state of the user's account on the blockchain, providing a secure authentication mechanism.

Clone this wiki locally