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 viaconfig.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 theaccount-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 thetimestamp
associated with the previous authentication for this user.- If the
timestamp
is less than or equal to thetimestamp
associated with the users previous authentication, your system must abort and reject the authentication attempt. - If the
timestamp
is greater than thetimestamp
associated with the users previous authentication, then continue. - Ensure the
timestamp
is reasonable. (e.g: Not atimestamp
associated with a time far in the future, or far in the past)
- If the
- If a user with the
address
has not authenticated with your application before, then continue.
- If a user with the
Your application must determine whether the
CompositeSignature
in theaccount-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 usingfcl.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 theaddress
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.