Skip to content

User identity

In this section, we're going to focus on identity. Specifically, we're going to create user identities using private-keys. The Textile Hub supports public-key infrastructure (PKI) allowing your app to support many user identity providers based on PKI (e.g. Metamask, 3Box, uPort, Blockstack) or derive your own. In most of our examples, we'll use a simple, platform agnostic keypair used in the libp2p project.

Key-based identity access

Your application can grant users access to your Hub APIs very easily. When doing so, the Hub can also help you verify that the users are who they claim to be using encryption. The general flow is as follows:

  1. A user attempts to sign-in by providing their public key.
  2. Your app creates a one-time challenge for the users.
  3. The user signs the challenge with their private key to generate credentials.
  4. Your app verifies the credentials.

Below, we will simplify these steps by using the Hub's helpful token generation endpoint that also does credential verification.

Generating an identity

In this example, we'll use an identity generated by the Libp2p crypto library and made available through the @textile/threads-core library.

Install dependency

npm install --save @textile/threads-core

Generating Identities

You can use the Libp2pCryptoIdentity utility to generate random new identities (private and public keys) and later, to sign challenges to prove private key ownership.

import {Libp2pCryptoIdentity} from '@textile/threads-core';

async function example () {
   /** Random new identity */
   const identity = await Libp2pCryptoIdentity.fromRandom()

   /** Convert to string. */
   const identityString = identity.toString()

   /** Restore an identity object from a string */
   const restored = Libp2pCryptoIdentity.fromString(identityString)
}

All of the instances above are different representations of the same user generated by Libp2pCryptoIdentity.fromRandom(). Each instance holds a different copy of the user's private-key and therefore should remain private between your app and your user.

Caching user identity

You can add simple client-side caching to store a user's identity in the browser and restore it when the user returns to the app.

Warning

localStorage isn't guaranteed and may be cleared by the browser, the system, or the users. Even more important, localStorage isn't a secure place to store secrets. You should provide alternative storage mechanisms if maintaining identity (and therefore data ownership and access) over time is important.

import {Libp2pCryptoIdentity} from '@textile/threads-core';

const getIdentity = async (): Promise<Libp2pCryptoIdentity> => {
  /** Restore any cached user identity first */
  const cached = localStorage.getItem("user-private-identity")
  if (cached !== null) {
    /** Convert the cached identity string to a Libp2pCryptoIdentity and return */
    return Libp2pCryptoIdentity.fromString(cached)
  }
  /** No cached identity existed, so create a new one */
  const identity = await Libp2pCryptoIdentity.fromRandom()
  /** Add the string copy to the cache */
  localStorage.setItem("identity", identity.toString())
  /** Return the random identity */
  return identity
}

Signing transactions

The Libp2pCryptoIdentity object contains a signing method, allowing your app to now sign arbitrary bytes for your users. You can create your own identity verification endpoint or use the Hub's token endpoint to verify the credentials.

import {Libp2pCryptoIdentity} from '@textile/threads-core';

async function sign (identity: Libp2pCryptoIdentity) {
   const challenge = Buffer.from('Sign this string');

   const credentials = identity.sign(challenge);

   return credentials
}

Next steps

Time to setup your app in development mode.

Advanced identity providers

Public key provider

interface Public {
  verify(data: Buffer, sig: Buffer): Promise<boolean>
}

interface Identity {
  sign(data: Buffer): Promise<Buffer>
  public: Public
}

Identity here represents any entity capable of signing a message. This is a simple public key infrastructure inspired interface that similarly requires the implementer to be capable of returning an associated public key for verification. In many cases, the Identity will just be a private key, but callers can use any setup that suits their needs. A default implementation based on Libp2p's crypto library is provided for convinience (and is also used by default if no identity is provided), however, many developers will want to use alternative identity provides, such as 3box/Ceramic, Fortmatic, and existing private/public keypair, or a web3 provider such as Metamask. Textile Hub also provides email-based identities.

3Box

One trick with the above workflow is that you need to help your users store and recover their private keys. You could do this with your own user model stored over an API. Alternatively, you can use any keypair manager, such as Metamask. There are a few of steps to generate a Textile compatible identity from the Metamask API. A good starting point is to use the 3Box SDK. 3Box manages a cluster of nodes that web3 users can push small bits of information to. In this approach, a user with a 3Box identity can use that identity to create and track Buckets or Threads generated on Textile.

Info

As of writing this, 3Box doesn't have Typescript typings available.

const Box = require("3box");

getIdentity = async (): Promise<Libp2pCryptoIdentity> => {
  /**
   * Initialize the 3Box API uses Metamask
   * This will allow the user to sign their transactions
   * Using Metamask and 3Box directly
   */
  const box = await Box.create((window as any).ethereum)
  const [address] = await (window as any).ethereum.enable()
  await box.auth([], { address });
  // Note: sometimes, openSpace returns early... caution
  const space = await box.openSpace('io-textile-dropzone');
  await box.syncDone;
  try {
    // We'll try to restore the private key if it's available
    var storedIdent = await space.private.get('identity');
    if (storedIdent === null) {
      throw new Error('No identity')
    }
    const identity = await Libp2pCryptoIdentity.fromString(storedIdent)
    return identity
  } catch (e) {
    /**
     * If the stored identity wasn't found, create a new one.
     */
    const identity = await Libp2pCryptoIdentity.fromRandom()
    const identityString = identity.toString()
    await space.private.set('identity', identityString);
    return identity
  }
}