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
~ yarnAt 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 ininitare complete and the authenticator is ready to accept login/logout requests. -
getStyle()- Gives you the ability to customize yourAuthenticatorand 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 newUserclass, verify the keys match the account provided, add theUserto 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
accountNameand callsrequiresGetKeyConfirmationto 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 theU2Fprotocol.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
accountNameparameter 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
lynxMobileobject into the browsers global window object, by accessinglynxMobilewe can callrequestSetAccountand 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 theUserorAuthenticatorclasses.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.usersto 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 theIdentityfrom 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.usersto 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-ledgeruses the eosjs-ledger-signature-provider to communicate with the Ledger device through the U2F protocol andual-scattersimply returns the keys it has already received from the inital call toscatter.getIdentity.Here are variations of
getKeys()ual-ledgerasync 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-scatterasync 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.transactin 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.

