Skip to content

Production mode

This section will cover how to work in a production environment with secure keys, unlike using insecure keys in development mode.

To work with API keys in a production environment:

  1. Generate a new API key and secret that has mandatory signing enabled.
  2. Setup an authorization endpoint that will hold your API secret and any optional user model for your app.
  3. Add a login step to your app that will use the new endpoint to authorize users in your app.

Differences from development mode

  • Users need a signature to accompany their API requests. Those signatures can only be created with your API secret and will expire.
  • You'll need to re-verify the signatures occasionally. For this, we'll move from using the withKeyInfo APIs to a new one called withUserAuth that can request updated signatures in your app.
  • withUserAuth is also designed to work without access to your secret, so your app can authorize users on your back-end and provide API key signatures on demand.

Other than that, all the APIs work the same way.

User identity

If you've followed the tutorials up until now, you're already using PKI, so your users will only ever share their public key with your API (or any API).

Therefore, you just need to verify that they hold the private key linked with the public key. Otherwise, users could spoof your system very easily.

From there, you can provide Hub API access to your users based on that verification.

Authentication server

Now, we'll setup a simple server that accepts a user's public key, verifies that they control the private key (via a challenge), and then grant the user access to the Hub APIs.

The user can pass the result (a UserAuth object) to the API and start creating Threads and Buckets.

Setup

There are a few resources you'll need before you start writing code.

  • An account. This is your developer account on the Hub.
  • A user group key. This is how your users access the Hub APIs. Consider creating the key in an organization and not your personal account so you can invite collaborators later.
  • A new Typescript project. We recommend using Typescript, as Textile libraries are in a stage rapid of development and type detection is valuable during upgrades.
  • A server framework. The example below uses KoaJS but it could just as easily be written with Express or the basic Node server.

Install dependencies

# Textile libraries
npm install --save @textile/hub

# Other utilities used in example
npm install --save dotenv emittery isomorphic-ws

# Libraries specific to our server framework, koajs
npm install --save koa koa-router koa-logger koa-json koa-bodyparser koa-route koa-websocket

Environment variables

We use a .env file in the root of our project repo. The values in this file will be pulled into the app each time it's run. This is where you'll add your Hub API key and secret.

Danger

The .env file should be added to your .gitignore so your key and secret are never shared.

Contents of .env.

USER_API_KEY=<insert user group key>
USER_API_SECRET=<insert user group secret>

Create the server

In our project setup, our main server is defined in src/index.ts. Unlike the simple credentials example, our server needs to handle two-way communication with the client during identity verification.

The flow is as follows:

  1. The client makes a login request.
  2. The server initiates the request with the Hub and gets back an identity challenge.
  3. The server passes the challenge to the client.
  4. The client confirms they own their private key by signing the challenge and passing it back to the server.
  5. The server passes it to the Hub.

    • If successful, a token is generated for the user and the server generates API credentials and passes the credentials, token, and API key back to the client.
  6. Now, the client can use the Hub APIs directly!

It sounds complicated, but you'll see it happens very fast with only a few lines of code.

In our example, we use websockets to enable the multi-step communication between the server and the client.

/** Provides nodejs access to a global Websocket value, required by Hub API */
;(global as any).WebSocket = require('isomorphic-ws')
import koa from "koa"
import Router from "koa-router"
import logger from "koa-logger"
import json from "koa-json"
import bodyParser from "koa-bodyparser"
import route from "koa-route"
import websockify from "koa-websocket"

import Emittery from "emittery"
import dotenv from "dotenv"

import { Client, UserAuth } from "@textile/hub"

/** Read the values of .env into the environment */
dotenv.config();

/** Port our server will run */
const PORT: number = 3000

/** Init Koa with Websocket support */
const app = websockify(new koa())

/** Middlewares */
app.use( json() )
app.use( logger() )
app.use( bodyParser() )

/**
 * Add websocket login endpoint
 */

/** Start the server! */
app.listen( PORT, () => console.log( "Server started." ) )

Add a websocket login handler

Next, we'll add a websocket endpoint to our server. Note the Add websocket login endpoint location in the server code above.

The primary step the server needs to do is accept a pubkey and issue a new challenge back to the client. When successful, new API credentials can be handed to the client.

View the full code example in the repo.

import { Client } from "@textile/hub";

async function example(pubkey: string) {
    /**
     * Init new Hub API Client with the user group API keys
     */
    const client = await Client.withKeyInfo({
        key: "USER_API_KEY",
        secret: "USER_API_SECRET",
    });

    /**
     * Request a token from the Hub based on the user public key */
    const token = await client.getTokenChallenge(
        pubkey,
        /** The callback passes the challenge back to the client */
        (challenge: Buffer) => {
            return new Promise((resolve, reject) => {
                // Send the challenge back to the client and
                // resolve(Buffer.from(sig))
                resolve();
            });
        }
    );
}

Now when you refresh your locally running server, you should have a websocket endpoint for client token creation.

Wrap-up

  • With the user verified in your system, you can keep their public key without any security issues.
  • However, you should never trust an API call only by the public key, the challenge step is critical.
  • The token provided in the response should be considered a secret that only should be shared with a single user. It does not expire.

Example on GitHub

If you'd like to learn more, we've provided a fully working example on GitHub:

Client (app) authentication

Now that our credentials endpoint is setup, we need to generate new credentials for each user's identity.

A basic client needs to be able to:

  • Submit a login request.
  • Handle a challenge request from the server.
  • Sign the challenge.
  • Return it over websockets.

We'll create a login function that handles the back and forth of the websocket and can combine with the withUserAuth function.

Login function

import { Buckets, Client, Identity, PrivateKey, UserAuth } from "@textile/hub";

/**
 * loginWithChallenge uses websocket to initiate and respond to
 * a challenge for the user based on their keypair.
 *
 * Read more about setting up user verification here:
 * https://docs.textile.io/tutorials/hub/web-app/
 */
const loginWithChallenge = (id: Identity) => {
    return (): Promise<UserAuth> => {
        return new Promise((resolve, reject) => {
            /**
             * Configured for our development server
             *
             * Note: this should be upgraded to wss for production environments.
             */
            const socketUrl = `ws://localhost:3001/ws/userauth`;

            /** Initialize our websocket connection */
            const socket = new WebSocket(socketUrl);

            /** Wait for our socket to open successfully */
            socket.onopen = () => {
                /** Get public key string */
                const publicKey = id.public.toString();

                /** Send a new token request */
                socket.send(
                    JSON.stringify({
                        pubkey: publicKey,
                        type: "token",
                    })
                );

                /** Listen for messages from the server */
                socket.onmessage = async (event) => {
                    const data = JSON.parse(event.data);
                    switch (data.type) {
                        /** Error never happen :) */
                        case "error": {
                            reject(data.value);
                            break;
                        }
                        /** The server issued a new challenge */
                        case "challenge": {
                            /** Convert the challenge json to a Buffer */
                            const buf = Buffer.from(data.value);
                            /** User our identity to sign the challenge */
                            const signed = await id.sign(buf);
                            /** Send the signed challenge back to the server */
                            socket.send(
                                JSON.stringify({
                                    type: "challenge",
                                    sig: Buffer.from(signed).toJSON(),
                                })
                            );
                            break;
                        }
                        /** New token generated */
                        case "token": {
                            resolve(data.value);
                            break;
                        }
                    }
                };
            };
        });
    };
};

const setupThreads = async (identity: Identity) => {
    /**
     * By passing a callback, the Threads library can refresh
     * the api signature whenever expiring.
     */
    const callback = loginWithChallenge(identity);
    const client = Client.withUserAuth(callback);
    client.getToken(identity);
    return client;
};

Convert Buckets from insecure API to secure API

If you're looking to convert your Buckets from using the insecure API to the secure one, see the code below:

Insecure keys example

When using your insecure API key, you typically initialized Buckets like the following:

import { Buckets, Identity, KeyInfo } from "@textile/hub";

const init = async (key: KeyInfo, identity: Identity) => {
    const buckets = await Buckets.withKeyInfo(key);
    await buckets.getToken(identity);
    return buckets;
};

Secure keys example

You'll now replace withKeyInfo and getToken with the single, withUserAuth method that requires the callback method:

import { Buckets, UserAuth } from "@textile/hub";

const init = (getUserAuth: () => Promise<UserAuth>) => {
    const buckets = Buckets.withUserAuth(getUserAuth);
    return buckets;
};

Wrap-up

Now you've had a chance to see how identities work with API keys to provide Hub resources to your users.

If you'd like to explore the examples explained above more, we've provided a fully working example on GitHub.

Example on GitHub

git clone git@github.com:textileio/js-examples.git
cd js-examples/hub-browser-auth-app