Introduction
A Wallet Provider handles Authentications and Authorizations. They play a very important role of being the place the users control their information and approve transactions.
One of FCLs core ideals is for the user to be in control of their data, a wallet provider is where many users will do just that.
FCL has been built in a way that it doesn't need to know any intimate details about a wallet provider up front, they can be discovered when the users wishes to let the dapp know about them. This gives us a concept we have been calling Bring Your Own Identity.
Identity
Conceptually, FCL thinks of identity in two ways: Public and Private.
Public identity will be stored on chain as a resource, it will be publicly available to anyone that knows the Flow Address for the account.
In FCL getting a users public identity will be as easy as:
_10import {user} from "@onflow/fcl"_10_10const identity = await user(flowAddress).snapshot()_10// ^_10// `------ The public identity for `flowAddress`_10_10const unsub = user(flowAddress).subscribe(identity => console.log(identity))_10// ^_10// `------- The public identity for `flowAddress`
Private identity will be stored by the Wallet Provider, it will only be available to the currentUser.
In FCL getting the currentUsers identity will fetch both the public and the private identities, merging the private into the public.
Private info needs to be requested via scopes before the challenge step, more on that later. We highly recommend Wallet Providers let the user see what scopes are being requested, and decide what scopes to share with the dapp.
Consumers of identities in FCL should always assume all data is optional, and should store as little as possible, FCL will make sure the users always see the latest.
_10import {config, currentUser, authenticate} from "@onflow/fcl"_10_10config.put("challenge.scope", "email") // request the email scope_10_10const unsub = currentUser().subscribe(identity => console.log(identity))_10// ^_10// `------- The private identity for the currentUser_10_10authenticate() // trigger the challenge step (authenticate the user via a wallet provider)
Identity Data
- All information in Identities are optional and may not be there.
- All values can be stored on chain, but most probably shouldn't be.
We would love to see Wallet Providers enable the user to control the following info publicly, sort of a public profile starter kit if you will.
FCL will always publicly try to fetch these fields when asked for a users information and it will be up to the Wallet provider to make sure they are there and keep them up to date if the user wants to change them.
name
-- A human readable name/alias/nym for a dapp users display nameavatar
-- A fully qualified url to a smaller image used to visually represent the dapp usercover
-- A fully qualified url to a bigger image, could be used by the dapp for personalizationcolor
-- A 6 character hex color, could be used by the dapp for personalizationbio
-- A small amount of text that a user can use to express themselves
If we can give dapp developers a solid foundation of usable information that is in the direct control of the users from the very start, which we belive the above fields would do, our hopes are they can rely more on the chain and will need to store less in their own database.
Private data on the other hand has more use cases than general data. It is pretty easy to imagine ordering something and needing information like contact details and where to ship something.
Eventually we would love to see that sort of thing handled completely on-chain, securely, privately and safely, but in the interm it probably means storing a copy of data in a database when its needed, and allowed by a user.
The process of a dapp receiving private data is as follows:
- The dapp requests the scopes they want up front
fcl.config().put("challenge.scope", "email+shippingAddress")
. - The User authenticates
fcl.authenticate()
and inside the Wallet Providers authentication process decides its okay for the dapp to know both theemail
and theshippingAddress
. The User should be able to decide which information to share, if any at all. - When the dapp needs the information they can request it from FCLs current cache of data, if it isnt there the dapp needs to be okay with that and adjust accodingly.
Below are the scopes we are thinking of supporting privately: FCL will only publicly and privately try to fetch these when specified up front by a dapp.
email
fullName
phone
textMessage
address
shippingAddress
location
publicKey
All of the above are still subject to change as it is still early days, we would like to work closely with Wallet Providers to produce a robust, detailed and consitent spec regarding scopes. Feedback and thoughts are always welcome.
Authentication Challenge
Authentication can happen one of two ways:
- Iframe Flow
- Redirection Flow
As a Wallet Provider you will be expected to register a URL endpoint (and some other information) with a handshake service (FCL will be launching with one in which registration happens on chain and is completely open source (Apache-2.0 lincense)).
This registered URL will be what is shown inside the iFrame or where the dapp users will be redirected.
For the remainder of this documentation we will refere to it as the Authentication Endpoint and pair it with the GET https://provider.com/flow/authentication
route.
The Authentication Endpoint will receive the following data as query params:
l6n
(required) -- location (origin) of dappnonce
(required) -- a random string supplied by the FCLscope
(optional) -- the scopes requested by the dappredirect
(optional) -- where to redirect once the authentication challenge is complete
_10GET https://provider.com/flow/authenticate_10 ?l6n=https%3A%2F%2Fdapp.com_10 &nonce=asdfasdfasdf_10 &scope=email+shippingAddress_10 &redirect=https%3A%2F%2Fdapp.com%2Fflow%2Fcallback_10_10The values will use javascripts `encodeURIComponent` function and scopes will be `+` deliminated.
We can tell that this challenge is using the Redirect Flow because of the inclusion of the redirect query param. The Iframe Flow will still need to be supported as it will be the default flow for dapps.
At this point its on the Wallet Provider to do their magic and be confident enough that the user is who they say they are.
The user should then be shown in some form what the dapp is requesting via the scopes and allow them to opt in or out of anything they want.
Once the Wallet Provider is ready to hand back control to the dapp and FCL it needs to complete the challenge by redirecting or emiting a javascript postMessage
event.
Redirecting will look like this:
_10GET https://dapp.com/flow/callback # supplied by the redirect query param above_10 ?l6n=https%3A%2F%2Fdapp.com # the l6n supplied by FCL above_10 &nonce=asdfasdfasdf # the nonce supplied by FCL above_10 &addr=0xab4U9KMf # address for the users flow account (if available) -- will be used to fetch public identity information and hooks_10 &padder=0xhMgqTff86 # address for the Wallet Providers account -- will be used to fetch provider information_10 &code=afseasdfsadf # a token supplied to FCL from the Wallet Provider, FCL will use this token when requesting private information and hooks, can be any url safe value_10 &exp=1650400809517 # when the code expires, a value of `0` will be considered as never expires_10 &hks==https%3A%2F%2Fprovider.com%2Fhooks # a URL where FCL can request the private information and hooks
Iframe will look like this:
_13parent.postMessage(_13 {_13 type: "FCL::CHALLENGE::RESPONSE", // used by FCL to know what kind of message this is_13 addr: "0xab4U9KMf",_13 paddr: "0xhMgqTff86",_13 code: "afseasdfsadf",_13 exp: 1650400809517,_13 hks: "https://provider.com/hooks",_13 nonce: "asdfasdfasdf",_13 l6n: decodeURIComponent(l6n),_13 },_13 decodeURIComponent(l6n)_13)
FCL should now have everything it needs to collect the Public, Private and Wallet Provider Info.
The Wallet Provider info will be on chain so its not something that needs to be worried about here by the Wallet Provider.
What does need to be worried about handling the hooks request which was supplied to FCL via the hks
value in the challenge response https://provider.hooks
.
The hooks request will be to the hks
value supplied in the challenge response. The request will also include the code as a query param
_10GET https://povider.com/hooks_10 ?code=afseasdfsadf
This request needs to happen for a number of reasons.
- If it fails FCL knows something is wrong and will attempt to re-authenticate.
- If is succeeds FCL knows that the code it has is valid.
- It creates a direct way for FCL to "verify" the user against the Wallet Provider.
- It gives FCL a direct way to get Private Identity Information and Hooks.
- The code can be passed to the backend to create a back-channel between the backend and the Wallet Provider.
When users return to a dapp, if the code FCL stored hasnt expired, FCL will make this request again in order to stay up to date with the latest informtaion. FCL may also intermitently request this information before some critial actions.
The hooks request should respond with the following JSON
_22const privateHooks = {_22 addr: "0xab4U9KMf", // the flow address this user is using for the dapp_22 keyId: 3, // the keyId the user wants to use when authorizing transaction_22 identity: { // the identity information fcl always wants if its there, will be deep merged into public info_22 name: "Bob the Builder",_22 avatar: "https://avatars.onflow.org/avatar/0xab4U9KMf.svg"_22 cover: "https://placekittens.com/g/900/300",_22 color: "cccc00",_22 bio: "",_22 },_22 scoped: { // the private info request in the original challenge_22 email: "bob@bob.bob", // the user said it was okay for the dapp to know the email_22 shippingAddress: null, // the user said it was NOT okay for the dapp to know the shippingAddress_22 },_22 provider: {_22 addr: "0xhMgqTff86", // the flow address for the wallet provider (used in the identity composite id)_22 pid: 2345432, // the wallet providers internal id for the user (used in the identity composite id)_22 name: "Super Wallet",_22 icon: "https://provider.com/assets/icon.svg",_22 authn: "https://provider.com/flow/authenticate",_22 }_22}
When FCL requested the Public info from the chain it is expecting something like this. It will be on the Wallet Provider to keep this information up to date.
_22const publicHooks = {_22 addr: "0xab4U9KMf",_22 keyId: 2,_22 identity: {_22 name: "Bob the Builder",_22 avatar: "https://avatars.onflow.org/avatar/0xab4U9KMf.svg"_22 cover: "https://placekittens.com/g/900/300",_22 color: "cccc00",_22 bio: "",_22 },_22 authorizations: [_22 {_22 id: 345324539,_22 addr: "0xhMgqTff86",_22 method: "HTTP/POST",_22 endpoint: "https://provider.com/flow/authorize",_22 data: {_22 id: 2345432_22 }_22 }_22 ]_22}
At this point FCL can be fairly confident who the currentUser is and is ready to initiate transactions the user can authorize.
Authorization
FCL will broadcast authorization requests to the Public and Private authorization hooks it knows for a User, in a process we call Asynchronous Remote Signing.
The core concepts to this idea are:
- Hooks tell FCL where to send authorization requests (Wallet Provider)
- Wallet Provider responds imediately with:
- a back-channel where FCL can request the results of the authorization
- some optional local hooks ways the currentUser can authorize
- FCL will trigger the local hooks if they are for the currentUser
- FCL will poll the back-channel requesting updates until an approval or denial is given
Below is the public authorization hook we received during the challenge above.
_10 {_10 id: 345324539,_10 addr: "0xhMgqTff86",_10 method: "HTTP/POST",_10 endpoint: "https://provider.com/flow/authorize",_10 data: {_10 id: 2345432_10 }_10 }
FCL will take that hook and do the following post requeset:
_14POST https://provider.com/flow/authorize_14 ?id=2345432_14---_14{_14 message: "...", // what needs to be signed (needs to be convered from hex to binary before signing)_14 addr: "0xab4U9KMf", // the flow address that needs to sign_14 keyId: 3, // the flow account keyId for the private key that needs to sign_14 roles: {_14 proposer: true, // this accounts sequence number will be used in the transaction_14 authorizer: true, // this transaction can "move" and "modify" the accounts resources directly_14 payer: true, // this transaction will be paid for by this account (also signifies that they are signing an envelopeMessage instead of a payloadMessage)_14 },_14 interaction: {...} // needed to recreate the message if the Wallet Provider wants to verify the message._14}
FCL ise expecting something like this in response:
_18{_18 status: "PENDING",_18 reason: null,_18 compositeSignature: null,_18 authorizationUpdates: {_18 method: "HTTP/POST",_18 endpoint: "https://provider.com/flow/authorizations/4323",_18 },_18 local: [_18 {_18 method: "BROWSER/IFRAME",_18 endpoint: "https://provider.com/authorizations/4324",_18 width: "300",_18 height: "600",_18 background: "#ff0066"_18 }_18 ]_18}
That local hook will be consumed by FCL, rendering an iframe with the endpoint as the src. If the user is already authenticated this screen could show them the Wallet Providers transaction approval process directly.
Because FCL isnt relying on any communication to or from the Iframe it can lock it down as much as possible, and remove it once the authorization is complete.
While displaying the local hook, it will request the status of the authorization from the authorizationUpdates
hook.
_10POST https://provider.com/flow/authorizations/4323
Expecting a response that has the same structure as the origin but without the local hooks:
_10{_10 status: "PENDING",_10 reason: "",_10 compositeSignature: null,_10 authorizationUpdates: {_10 method: "HTTP/POST",_10 endpoint: "https://provider.com/flow/authorizations/4323",_10 },_10}
FCL will then follow the new authorizationUpdates
hooks until the status changes to "APPROVED"
or "DECLINED"
.
If the authorization is declined it should include a reason if possible.
_10{_10 status: "DECLINED",_10 reason: "They said no",_10}
If the authorization is approved it should include a composite signature:
_10{_10 status: "APPROVED",_10 compositeSignature: {_10 addr: "0xab4U9KMf", // the flow address that needs to sign_10 keyId: 3, // the flow account keyId for the private key that needs to sign_10 signature: "..." // binary signature of message encoded as hex_10 }_10}
FCl can now submit the transaction to the Flow blockchain.
TL;DR Wallet Provider
Register Provider with FCL Handshake and implement 5 Endpoints.
GET flow/authenticate
->parent.postMessage(..., l6n)
GET flow/hooks?code=___
->{ ...identityAndHooks }
POST flow/authorize
->{ status, reason, compositeSignature, authorizationUpdates, local }
POST authorizations/:authorization_id
GET authorizations/:authorization_id