MAINNET:
Loading...
TESTNET:
Loading...
/
onflow.org
Flow Playground

Proving Ownership of a Flow Account


Proving Ownership of a Flow Account

A common desire that application developers have is to be able to prove that a user controls an on chain account. Fortunately, FCL provides multiple ways to achieve this.

During user authentication, some FCL compatible wallets will choose to support the FCL account-proof service. If a wallet chooses to support this service, they will include within it specific pieces of information that you can use to prove that a user controls an on chain account. The service, and its data within it, will be returned from the wallet during authentication, and made available for your use in FCL Current User.

If a user's wallet does not choose to support the account-proof service, you can still prove that a user controls an on chain account provided that the wallet does support the user-signature service. Since all wallets may optionally choose to support either of these services, your application should be resilient enough to handle cases where a user's wallet supports one, both or neither.

The account-proof service, as the name implies, was designed specifically for proving authentication, while user-signature was designed for more generic use cases. We suggest that your application choose to use account-proof service first, if it's available, and then fall back to user-signature if needed.

We'll walk through how you, an application developer, can use either service to prove a user's authentication.

Are you an FCL Wallet Developer? Check out the wallet provider specific docs here

Proving User Authentication using account-proof

The suggested order of operations of how your application might use account-proof begins with:

  • Optional: Your application may choose to configure FCL with an application specific Domain Separation Tag. A custom configured Domain Separation Tag helps scope the cryptographic proof generated for the account-proof service to your application. A Domain Separation Tag is a valid string less than 32 bytes long. (Note, inclusion of a Domain Separation Tag does not help to prevent Man-in-the-middle attacks.) Your application can specify a custom Domain Separation Tag via config.fcl.appDomainTag.
import {config} from "@onflow/fcl"

config({
  "fcl.appDomainTag": "AWESOME-APP-V0.0-user",
})
  • Your application calls fcl.authenticate() to begin the user authentication process.
  • Your user's wallet will optionally return the account-proof service as part of authentication.
  • Your application extracts the data within the account-proof service.

The data within the account-proof service will look like:

{
  f_type: "Service",  // Its a service!
  f_vsn: "1.0.0",  // Follows the v1.0.0 spec for the service
  type: "account-proof",  // the type of service it is
  method: "DATA",  // Its data!
  uid: "amazing-wallet#account-proof",  // A unique identifier for the service
  data: {
    f_type: "account-proof",
    f_vsn: "1.0.0"
    signatures: [CompositeSignature],
    address: "0xUSER",  // The user's address
    timestamp: 1630705495551, // UNIX timestamp
    appDomainTag: "AWESOME-APP-V0.0-user", // Optional
  }
}
  • Your application will send the account-proof service data structure to it's backend over a secure channel.

Once your application receives the account-proof data structure, it can then begin to use it to prove user authentication. The steps that your application's backend must follow in using this data are specific.

  • First your application must determine wether a user with the address provided in the account-proof data structure has authenticated with your application before.

    • If a user with the address has authenticated with your application before, then your system must query the timestamp associated with the previous authentication for this user.
      • If the timestamp is less than or equal to the timestamp associated with the users previous authentication, your system must abort and reject the authentication attempt.
      • If the timestamp is greater than the timestamp associated with the users previous authentication, then continue.
      • Ensure the timestamp is reasonable. (e.g: Not a timestamp associated with a time far in the future, or far in the past)
    • If a user with the address has not authenticated with your application before, then continue.
  • Your application must determine whether the CompositeSignature in the account-proof data structure contains a valid signature. Your application will do this by:

    • Using FCL's provided utility for reconstructing the data that was signed to produce the CompositeSignature:

      import {WalletUtils} from "@onflow/fcl"
      
      const Address = AccountProof.address
      const Timestamp = AccountProof.timestamp
      
      const Message = WalletUtils.encodeMessageForProvableAuthnVerifying(
        Address, // Address of the user authenticating
        Timestamp, // Timestamp associated with the authentication
        "AWESOME-APP-V0.0-user" // Application domain tag
      )
      
    • Alternatively you can manually reconstruct the data that was signed to produce the CompositeSignature. Pseudocode:

      import rlp from "@onflow/rlp"
      
      const Address = AccountProof.address
      const Timestamp = AccountProof.timestamp
      
      const rightPaddedHexBuffer = (value, pad) =>
        Buffer.from(value.padEnd(pad * 2, 0), "hex")
      
      const CUSTOM_APP_DOMAIN_TAG = "AWESOME-APP-V0.0-user"
      const DomainTag = rightPaddedHexBuffer(
        Buffer.from(CUSTOM_APP_DOMAIN_TAG).toString("hex"),
        32
      ).toString("hex")
      
      const Message = DomainTag + rlp([Address, Timestamp])
      
    • Verify that the account-proof signature is valid for the reconstructed data. Your application can do this through executing it's own Cadence script, or using fcl.verifyUserSignature:

...
const isValid = await fcl.verifyUserSignatures(
    Message,
    AccountProof.signatures
)
  • If the signature has been proven valid, your application has then successfully proved that the user does control the account with address they authenticated with.
  • Your application must finally save the AccountProof.timestamp associated with this authentication attempt alongside the address for the user for future reference.

Proving User Authentication using user-signature

If your user's wallet does not return the the FCL account-proof service, your application can still prove user authentication using the user-signature service, as long as it is supported by your user's wallet.

Your application's backend must first generate a random message for the user to sign with their wallet, using the user-signature service. Your application should retain this random message and associate it with the address of the account the user is authenticating with for future reference.

Your application will then pass this message to it's frontend, where it will call:

const CompositeSignatures = await fcl.currentUser().signUserMessage(Message)

Once the user's wallet has signed the message, your application will then verify that the signature is correct. It will do this by passing the CompositeSignatures to your applications backend. Your application will then execute it's own Cadence script, or otherwise use fcl.verifyUserSignature:

const isValid = await fcl.verifyUserSignatures(Message, CompositeSignatures)

If the signature has been proved valid, your application has then successfully proved that the user does control the account with address they authenticated with.