This tutorial walks through the steps required to create a UAL for Ledger Authenticator.
EOSIO Labs repositories are experimental. Developers in the community are encouraged to use EOSIO Labs repositories as the basis for code and concepts to incorporate into their applications. Community members are also welcome to contribute and further develop these repositories. Since these repositories are not supported by Block.one, we may not provide responses to issue reports, pull requests, updates to functionality, or other requests from the community, and we encourage the community to take responsibility for these.
The Universal Authenticator Library creates a single universal API which allows app developers to integrate multiple signature providers with just a few lines of code. This is done through custom Authenticators
.
An Authenticator
represents the bridge between UAL and a custom signing method.
A developer that wishes to add support for their signature provider to UAL must create an Authenticator
by implementing 2 classes. An Authenticator
and a User
.
The Authenticator
class represents the business logic behind the renderer, handles login/logout functionality and initializes the User
class.
Logging in returns 1 or more User objects. A User
object provides the ability for an app developer to request the app User
sign a transaction using whichever authenticator they selected when logging in.
In this tutorial I'll walk through the steps of implementing a custom UAL Authenticator
, we'll be creating a ual-ledger Authenticator. I'll try to explain some of the implementation specific details for ual-ledger
and show examples of other UAL Authenticators.
Each step in this tutorial has a correlating branch on github labeled step-1
, step-2
, etc. Each step assumes you are starting at the correlating branch.
At the end we'll test the custom Authenticator with an example app found in example/app.
~ git clone [email protected]:EOSIO/ual-authenticator-walkthrough.git
~ cd ual-authenticator-walkthrough/examples/authenticator
~ yarn
At this point you should have a basic folder structure that looks like this.
Create a new class Ledger
in src/Ledger.js
thats extends from the Authenticator
class and add empty functions for all the abstract methods.
Next we'll do the same thing for the LedgerUser
in src/LedgerUser.js
that extends from the User
class.
Export both files from src/index.js
with the contents below.
export * from './Ledger'
export * from './LedgerUser'
View the completed Ledger.js
View the completed LedgerUser.js
The internal business logic of each Authenticator method will depend on the signing method you are using. The only limitations are the input/return types must match the abstract method it is implementing.
Although not all methods may be necessary for your Authenticator
, you're required to implement all abstract methods from the base Authenticator class.
The key methods here are init, getStyle, login, logout
.
-
init()
- Should be used to handle any async operations required to initialize the authenticator.isLoading()
should return true until all async operations ininit
are complete and the authenticator is ready to accept login/logout requests. -
getStyle()
- Gives you the ability to customize yourAuthenticator
and how it is displayed to app users.getStyle() { return { // An icon displayed to app users when selecting their authentication method icon: './custom-icon.png', // Name displayed to app users text: 'Ledger', // Background color displayed to app users who select your authenticator background: '#44bdbd', // Color of text used on top the `backgound` property above textColor: '#FFFFFF', } }
-
login()
- The implementation depends entirely on the signing method you are using, whether it supports multiple chains, and the communication protocol used. You'll need to create a newUser
class, verify the keys match the account provided, add theUser
to an array, and return the array ofUser
's. Otherwise throw an error with the appropriate messaging, this error will be displayed to the app user.Here are variations of
login()
with a brief description of the different approaches.-
ual-ledger - Ledger requires an
accountName
and callsrequiresGetKeyConfirmation
to determine if the app user has already confirmed the public key from their ledger device, if so they won't need to give permission again. By callingLedgerUser.isAccountValid()
the authenticator utilizes the eosjs-ledger-signature-provider and communicates with the ledger device through theU2F
protocol.async login(accountName) { for (const chain of this.chains) { const user = new LedgerUser(chain, accountName, this.requiresGetKeyConfirmation(accountName)) await user.init() const isValid = await user.isAccountValid() if (!isValid) { const message = `Error logging into account "${accountName}"` const type = UALErrorType.Login const cause = null throw new UALLedgerError(message, type, cause) } this.users.push(user) } return this.users }
-
ual-scatter - Scatter does not require an
accountName
parameter and uses the Scatter-JS library to communicate with Scatter Desktop.async login() { try { for (const chain of this.chains) { const user = new ScatterUser(chain, this.scatter) await user.getKeys() this.users.push(user) } return this.users } catch (e) { throw new UALScatterError( 'Unable to login', UALErrorType.Login, e) } }
-
ual-lynx - Lynx injects a
lynxMobile
object into the browsers global window object, by accessinglynxMobile
we can callrequestSetAccount
and receive an object containing the account information of the account logged into the Lynx Wallet.async login() { if (this.users.length === 0) { try { const account = await window.lynxMobile.requestSetAccount() this.users.push(new LynxUser(this.chains[0], account)) } catch (e) { throw new UALLynxError( 'Unable to get the current account during login', UALErrorType.Login, e) } } return this.users }
-
-
logout()
- Responsible for terminating connections to external signing methods, if any exist, and deleting user information that may have been cached in theUser
orAuthenticator
classes.Variations of
logout()
-
ual-ledger - The eosjs-ledger-signature-provider performs a simple caching of public keys that need to be cleared on logout. We accomplish this by calling
signatureProvider.clearCachedKeys()
and remove the logged in users by reassigningthis.users
to an empty array.async logout() { try { for (const user of this.users) { user.signatureProvider.cleanUp() user.signatureProvider.clearCachedKeys() } this.users = [] } catch (e) { const message = CONSTANTS.logoutMessage const type = UALErrorType.Logout const cause = e throw new UALLedgerError(message, type, cause) } }
-
ual-scatter - Calling
this.scatter.logout()
removes theIdentity
from scatter utilizing scatters built in method for logging out.async logout() { try { this.scatter.logout() } catch (error) { throw new UALScatterError('Error occurred during logout', UALErrorType.Logout, error) } }
-
ual-lynx - Since lynx does not provide a method of logging out we simple reassign
this.users
to an empty array.async logout() { this.users = [] }
-
View the completed Ledger.js
You are required to implement all abstract methods from the base User class.
The main methods to be implemented here are getKeys, signTransaction, signArbitrary
.
-
getKeys()
- Calling this method should return an array of public keys 🔑. How the authenticator gets those keys depends on the signing method you are using and what protocol it uses. For example,ual-ledger
uses the eosjs-ledger-signature-provider to communicate with the Ledger device through the U2F protocol andual-scatter
simply returns the keys it has already received from the inital call toscatter.getIdentity
.Here are variations of
getKeys()
ual-ledger
async getKeys() { try { const keys = await this.signatureProvider.getAvailableKeys(this.requestPermission) return keys } catch (error) { const message = `Unable to getKeys for account ${this.accountName}. Please make sure your ledger device is connected and unlocked` const type = UALErrorType.DataRequest const cause = error throw new UALLedgerError(message, type, cause) } }
ual-scatter
async getKeys() { if (!this.keys || this.keys.length === 0) { // `refreshIdentity` calls `scatter.getIdentity` then // sets the `keys` and `accountName` properties on the // `User` class await this.refreshIdentity() } return this.keys }
-
signTransaction(transaction, config)
- Exposes the same API asApi.transact
in eosjs. -
signArbitrary(publicKey, data, helpText)
- A utility function to sign arbitrary data. If your authenticator does not support this type of signing you can simple return an error with the correct message.Example of an authenticator that does not support
signArbitrary
// LedgerUser.js async signArbitrary() { throw new UALLedgerError( `${Name} does not currently support signArbitrary`, UALErrorType.Unsupported, null, ) }
View the completed LedgerUser.js
Now that we've implemented all the abstract methods on our Ledger
and LedgerUser
classes lets test them in the example react app provided in examples.
Go to examples and follow the instructions.
All product and company names are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.
Check out the Contributing guide and please adhere to the Code of Conduct
See LICENSE for copyright and license terms. Block.one makes its contribution on a voluntary basis as a member of the EOSIO community and is not responsible for ensuring the overall performance of the software or any related applications. We make no representation, warranty, guarantee or undertaking in respect of the software or any related documentation, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall we be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or documentation or the use or other dealings in the software or documentation. Any test results or performance figures are indicative and will not reflect performance under all conditions. Any reference to any third party or third-party product, service or other resource is not an endorsement or recommendation by Block.one. We are not responsible, and disclaim any and all responsibility and liability, for your use of or reliance on any of these resources. Third-party resources may be updated, changed or terminated at any time, so the information here may be out of date or inaccurate. Any person using or offering this software in connection with providing software, goods or services to third parties shall advise such third parties of these license terms, disclaimers and exclusions of liability. Block.one, EOSIO, EOSIO Labs, EOS, the heptahedron and associated logos are trademarks of Block.one.
Wallets and related components are complex software that require the highest levels of security. If incorrectly built or used, they may compromise users’ private keys and digital assets. Wallet applications and related components should undergo thorough security evaluations before being used. Only experienced developers should work with this software.