Skip to content

Instantly share code, notes, and snippets.

@orodio
Created July 6, 2021 17:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save orodio/a74293f65e83145ec8b968294808cf35 to your computer and use it in GitHub Desktop.
Save orodio/a74293f65e83145ec8b968294808cf35 to your computer and use it in GitHub Desktop.
FCL Compatible Wallets

Authentication

In FCL wallets are configured by passing in a wallet providers authentication URL to the discovery.wallet config. You as someone who is making an FCL compatible wallet will need to make and expose an authentication endpoint that will be rendered in an iframe.

// IN APPLICATION
// configuring fcl to point at a wallet looks like this
import {config} from "@onflow/fcl"

config({
  "discovery.wallet": "your-url-that-fcl-will-render-in-iframe"
})

Once the page is rendered and ready to do things, the wallet needs to tell FCL that it is ready, you will do this by sending a post message to FCL, and FCL will send one back with some extra info you can use to know about the application requesting authentication on behalf of the user.

// IN WALLET AUTHENTICATION FRAME

function callbackback({ data }) {
  // early exit if it isnt the FCL:AUTHN:CONFIG message
  if (typeof data != "object") return
  if (typeof data.type !== "FCL:AUTHN:CONFIG") return

  // this might have a bunch of usefull information  
  doSomethingWithConfigData(e.data)

  // always clean up your eventlisteners
  window.removeEventListener(callback)
}
// add event listener first
window.addEventListener("message", callback)

// tell fcl the wallet is ready
window.parent.postMessage({type: "FCL:FRAME:READY"}, "*")

You will learn fairly fast that almost everything in FCL is optional, the config that FCL will send here is the applications chance to suggest to you the wallet what they would like you to send them. An example of that is the OpenID service, the application can via that config request for you the wallet to send them the email address of the current user, them requesting this does not mean you need to send it. In the config they can also tell you the wallet a couple things about them the application like the name of their application or a url for an icon so you can customize things slightly. You the wallet having a visual distinction but a seemless and connected experience is our goal here. As always never trust anything you recieve from an application, always do your due-dilligence and be onguard, you as the wallet are the users first line of defense.

We have configuration stuff

At this point its important that you as the wallet are confident that the user is the user. Have them provide enough proof to you that you are okay with passing their details back to FCL. If we were to use Blocto as an example, this is where the user is actually loggin into Blocto, putting in their email, getting sent the code, everything Blocto needs to do to be confident in the users identity.

You know who the user is!!

Once you confident in the users identity, we can complete this authentication process. The authentication process is complete once FCL receives back a post message that configures it for the user. This response is everything to FCL, at its core it tells FCL who the user is and then via included services it includes how the user authenticated, how to request transaction signatures, how to get a personal message signed, the email and other details if requested, and in the future it may inlude many more things. You can kind of think of FCL as a plugin system, but all the plugins exist elsewhere, so FCL needs to communicate with those plugins. What you are sending back to FCL is everything that it needs to communicate with the plugins you are supplying. Your wallet is a plugin to FCL, these details tell FCL how to use you as a plugin.

// IN WALLET AUTHENTICATION FRAME
window.postMessage({
  type: "FCL:FRAME:RESPONSE",          // The message type FCL is expecting
  addr: "0xUSER",                      // The users flow address

  services: [                          // All the stuff that configures FCL
    
    // Authentication Service - REQUIRED
    {
      f_type: "Service",                                   // Its a service!
      f_vsn: "1.0.0",                                      // Follows the v1.0.0 spec for the service
      type: "authn",                                       // the type of service it is
      method: "DATA",                                      // Its data!
      uid: "amazing-wallet#authn",                         // A unique identifier for the service
      endpoint: "your-url-that-fcl-will-render-in-iframe", // should be the same as was passed into the config
      id: "0xUSER",                                        // the wallets internal id for the user, use flow address if you dont have one
      // The Users Info
      identity: {
        f_type: "Identity",  // Its an Identity!
        f_vsn: "1.0.0",      // Follows the v1.0.0 spec for an identity
        address: "0xUSER",   // The users address
        keyId: 0,            // OPTIONAL - The Users KeyId they will use
      },
      // The Wallets Info
      provider: {
        f_type: "ServiceProvider",      // Its a Service Provider
        f_vsn: "1.0.0",                 // Follows the v1.0.0 spec for service providers
        address: "0xWallet",            // A flow address owned by the wallet
        name: "Amazing Wallet",         // OPTIONAL - The name of your wallet. ie: "Dapper Wallet" or "Blocto Wallet"
        description: "The best wallet", // OPTIONAL - A short description for your wallet
        icon: "https://___",            // OPTIONAL - Image url for your wallets icon
        website: "https://___",         // OPTIONAL - Your wallets website
        supportUrl: "https://___",      // OPTIONAL - An url the user can use to get support from you
        supportEmail: "help@aw.com",    // OPTIONAL - An email the user can use to get support from you
      },
    },

    // Authorization Service
    {
      f_type: "Service",
      f_vsn: "1.0.0",
      type: "authz",
      uid: "amazing-wallet#authz",
      // We will cover this at length in the authorization section of this guide
    },
    
    // User Signature Service
    {
      f_type: "Service",
      f_vsn: "1.0.0",
      type: "user-signature",
      uid: "amazing-wallet#user-signature",
      // We will cover this at length in the user signature section of this guide
    },

    // OpenID Service
    {
      f_type: "Service",
      f_vsn: "1.0.0",
      type: "open-id",
      uid: "amazing-wallet#open-id",
      method: "DATA",
      data: { // only include data that was request, ideally only if the user approves the sharing of data, everything is optional
        f_type: "OpenID",
        f_vsn: "1.0.0",
        profile: {
          name: "bob",
          family_name: "builder", // icky underscored names because of OpenID Connect spec
          given_name: "robert",
          middle_name: "the",
          nickname: "bob the builder",
          preferred_username: "bob",
          profile: "https://www.bobthebuilder.com/",
          picture: "https://avatars.onflow.org/avatar/bob-the-builder",
          website: "https://www.bobthebuilder.com",
          gender: "small cartoonish man",
          birthday: "1999-01-30", // can use 0000 for year if year is not known
          zoneinfo: "America/Vancouver", // they are so inconsistent :(
          locale: "en",
          updated_at: "1625588304427"
        },
        email: {
          email: "bob@bob.bob",
          email_verified: false,
        }
      },
    }
  ]
}, "*")

Stoping an authentication Process.

From any frame, you can send a FCL:FRAME:CLOSE message to FCL which will halt the current routine and close the frame.

window.parent.postMessage({ type: "FCL:FRAME:CLOSE" }, "*")

Service Methods

In the above Authenitaction section you will have been introduced to services. Services are your main way of configuring FCL. Sometimes they just configure it and thats it, an example of that can be seen with the Authn Service and the OpenID Service. With those two services you as the wallet are just saying "here is a bunch of info". You will see that those two services both have a method: "DATA" field in them. Currently in those two case they can only be a data service.

Other services can be a little more complex, they can require a back and forth between FCL and the Service in question. Ultimately we want to do this back and forth via a secure back-channel, http requests to servers, but in some situations that isn't a viable option so there is also a front-channel option. Where possible you as a wallet provider should aim to provide a back-channel support for services, and only fall back to a front-channel if absolutely necessary.

Back-channel communication use method: "HTTP/POST", while front-channel communication use method: "IFRAME/RPC".

IFRAME/RPC

IFRAME/RPC is the easiest to explain so we will start with that. You have more or less already been doing this with authentication (we will eventually enable back-channel for authentication).

  • An iframe is rendered (comes from endpoint in the service).
  • The rendered frames says its ready window.parent.postMessage({type: "FCL:FRAME:READY"}, "*").
  • FCL will send the data to be dealt with frame.postMessage({ type: "FCL:FRAME:READY:RESPONSE", ...body, service: {params, data} }, "*")
    • Where body is the stuff you care about, params and data are things you can provide in the service object.
  • The wallet sends back an Approved or Declined post message (It will be a f_type: "PollingResponse", we will get to that in a bit)
    • If its approved the polling responses data field will need to be what FCL is expecting
    • If its declined the polling responses reason field should say why it was declined

HTTP/POST

HTTP/POST initially sends a post request to the endpoint specified in the service, which should imediately return a f_type: "PollingResponse". Like the IFRAME/RPC our goal is to eventually get an APPROVED or DECLINED polling response, and technically this endpoint could return one of those immediately. But more than likely that isn't the case and it will be in a PENDING state (PENDING is not available to IFRAME/RPC). When the polling response is PENDING it requires and update field that includes a service (BackChannelRpc) FCL can use to request an updated PollingResponse from. FCL will use that BackChannelRpc to request a new PollingResponse which itself can be APPROVED, DECLINED or PENDING. If it is APPROVED or DECLINED FCL will halt and return/error, but if it is PENDING it will use the BackChannelRpc supplied in the new PollingResponse update field. It will repeat this cycle until it is either APPROVED or DECLINED.

There is an additional feature that HTTP/POST enables in the first PollingResponse that is returned. This feature is the ability for FCL to render an iframe and it can be triggered by supplying a service type: "FRAME" and the endpoint that the wallet wishes to render.

Polling Response

// APPROVED
{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "APPROVED",
  data: ___, // what the service needs to send to FCL
}

// Declined
{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "DECLINED",
  reason: "Declined by user."
}

// Pending - Simple
{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "PENDING",
  updates: {
    f_type: "Service",
    f_vsn: "1.0.0",
    type: "back-channel-rpc",
    endpoint: "https://____", // where post request will be sent
    method: "HTTP/POST",
    data: {},   // will be included in the requests body
    params: {}, // will be included in the requests url
  }
}

// Pending - First Time with Local
{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "PENDING",
  updates: {
    f_type: "Service",
    f_vsn: "1.0.0",
    type: "back-channel-rpc",
    endpoint: "https://____", // where post request will be sent
    method: "HTTP/POST",
    data: {},   // included in body of request
    params: {}, // included as query params on endpoint
  },
  local: {
    f_type: "Service",
    f_vsn: "1.0.0",
    type: "frame",
    endpoint: "https://____", // the iframe that will be rendered,
    data: {}, // sent to frame when ready
    params: {}, // included as query params on endpoint
  }
}

data and params

data and params are information that the wallet can provide in the service config that FCL will pass back to the service.

  • params will be added onto the endpoint as query params.
  • data will be included in the body of the HTTP/POST request or in the FCL:FRAME:READY:RESPONSE for an IFRAME/RPC

Authorization Service

Authorization services are depicted with with a type: "authz" and a method of either HTTP/POST or IFRAME/RPC. They are expected to eventually return a f_type: "CompositeSignature".

An authorization service is expected to know the Account and the Key that will be used to sign the transaction at the time the service is sent to FCL (during authentication).

{
  f_type: "Service",
  f_vsn: "1.0.0",
  type: "authz",               // say its an authorization service
  uid: "amazing-wallet#authz", // standard service uid
  method: "HTTP/POST",         // can also be `IFRAME/RPC`
  endpoint: "https://____",    // where to talk to the service
  identity: {
    f_type: "Identity",
    f_vsn: "1.0.0",
    address: "0xUser",         // the address that the signature will be for
    keyId: 0,                  // the key for the address that the signature will be for
  },
  data: {},
  params: {},
}

FCL will use the method provided to request a composite signature from authorization service (Wrapped in a PollingResponse). The authorization service will be sent a Signable. The service is expected to construct an encoded message to sign from Signable.voucher. It then needs to hash the encoded message, tag the message, and then sign it producing a signature. The signature needs to be sent back to FCL as a HEX string in the response object (which is a CompositeSignature).

siganture = 
  signable.voucher
    |> encode
    |> hash
    |> tag
    |> sign
    |> convert_to_hex

The eventual response back from the authorization service should resolve to something like this:

{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "APPROVED",
  data: {
    f_type: "CompositeSignature",
    f_vsn: "1.0.0",
    addr: "0xUSER",
    keyId: 0,
    signature: "signature as hex value"
  }
}

User Signature Service

User Signature services are depicted with a type: "user-signature" and a method of either HTTP/POST or IFRAME/RPC. They are expected to eventually return an array of f_type: "CompositeSignature".

The User Signature service is a stock/standard service.

{
  f_type: "Service",
  f_vsn: "1.0.0",
  type: "user-signature",               // say its an user-signature service
  uid: "amazing-wallet#user-signature", // standard service uid
  method: "HTTP/POST",                  // can also be `IFRAME/RPC`
  endpoint: "https://___",              // where to talk to the service
  data: {},
  params: {},
}

FCL will use the method provided to request an array of composite signatures from the user signature service (Wrapperd in a PollingResponse). The user signature service will be sent a Signable. The service is expected to tag the Signable.message and then sign it with enough keys to produce a full weight. The signatures need to be sent back to FCL as HEX strings in an array of CompositeSignatures.

# per required signature
signature =
  signable.message
    |> tag
    |> sign
    |> convert_to_hex

The eventual response back from the user signature service should resolve to something like this:

{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "APPROVED",
  data: [
    {
      f_type: "CompositeSignature",
      f_vsn: "1.0.0",
      addr: "0xUSER",
      keyId: 0,
      signature: "signature as hex value"
    },
    {
      f_type: "CompositeSignature",
      f_vsn: "1.0.0",
      addr: "0xUSER",
      keyId: 1,
      signature: "signature as hex value"
    }
  ]
}

Pre Authz Service

This is a strange one, but extremely powerful. This service should be used when the wallet is responsible for multiple roles of a transaction but want the ability to change the accounts on a per role basis.

Pre Authz Services are depicted with a type: "pre-authz" and a method of either HTTP/POST or IFRAME/RPC. They are expected to eventually return a f_type: "PreAuthzResponse".

The Pre Authz Service is a stock/standard service.

{
  f_type: "Service",
  f_vsn: "1.0.0",
  type: "pre-authz",               // say its a pre-authz service
  uid: "amazing-wallet#pre-authz", // standard service uid
  method: "HTTP/POST",             // can also be IFRAME/RPC
  endpoint: "https://___",         // where to talk to the service
  data: {},
  params: {},
}

FCL will use the method provided to request a PreAuthzReponse (Wrapped in a PollingResponse). The Authorizations service will be sent a PreSignable. The pre-authz service is expected to look at the PreSignable and determine the breakdown of accounts to be used. The pre-authz service is expected to return Authz services for each role it is responsible for. A pre-authz service can only supply roles it is responsible for. If a pre-authz service is responsible for multiple roles, but it wants the same account to be responsible for all the roles, it will need to supply an Authz service per role.

The eventaul response back from the pre-authz service should resolve to something like this:

{
  f_type: "PollingResponse",
  f_vsn: "1.0.0",
  status: "APPROVED",
  data: {
    f_type: "PreAuthzResponse",
    f_vsn: "1.0.0",
    proposer: {              // A single Authz Service
      f_type: "Service",
      f_vsn: "1.0.0",
      type: "authz",
    },
    payer: [                // An array of Authz Services
      {
        f_type: "Service",
        f_vsn: "1.0.0",
        type: "authz",
      }
    ],
    authorization: [       // An array of Authz Serivces (its singular because it only represents a singular authorization)
      {
        f_type: "Service",
        f_vsn: "1.0.0",
        type: "authz",
      }
    ],
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment