Authentication Walkthrough

The Blockset API uses standard cryptographic building blocks, public/private keys pairs, and JSON Web Tokens (JWT) in order to manage authentication.

Authentication breaks down into three object types: Account, Client, & User.

Account

Service-Provider Level

Accounts are the "billing-level" user object. Every request must be associated with an account, so it can be correlated to rate limits and an entity for accounting purposes. For example, “FancyCorp” will have a single Account. Generally Accounts are maintained by BRD and the service provider will not have to interact with it much besides for testing. By default, Accounts start with low rate limits, allowing 10 requests per second, under the leaky bucket algorithm. This rate can be upgraded by contacting our sales department.

Client

Application Level

Clients are the "code-level" unit of users. Every codebase (i.e. Web, iOS, Android) should have its own Client to represent it in the Blockset system. These are created and owned by Accounts and represent an application created by the Account holders. For example, the “FancyCorp” Account may have two Clients: “FancyCorp iOS” and “FancyCorp Android”. Clients should only be created once per app. By default, Clients start with low rate limits, allowing 1 request per second. This rate can be upgraded by contacting our sales department.

User

User Level

User objects are created by Clients and represent individual users of the client. For example, for every installation of “FancyCorp Android” - the app must create and maintain a User token for that person. By default, Users start with low rate limits, allowing 1 request per minute. This rate can be upgraded by contacting our sales department.

A User may have multiple devices, but only one private/public key pair. The device IDs are encoded into the JWTs used for authentication, but are not directly associated with a User.

Each of these principal types have their own JWT and UUID. The Account JWT and Client JWT are provided by the server while the User JWT is signed client-side using the user’s own signing key. This schema (client-side-generated JWTs) allows for password-less API access.

Step 1. Begin by obtaining your Account JWT

Create an account using POST /accounts or find your token using POST /accounts/login

const getAccountJwt = async (email, password) => {
  const body = { email, password }
  const response = await axios.post('https://api.blockset.com/accounts/login', body)
  return response.data.token
}

Step 2. Generate a Client JWT

Given an Account JWT, you may generate a Client JWT utilizing POST /clients

const getClientJwt = async (accountJwt) => {
  const headers = { authorization: `Bearer ${accountJwt}` }
  const body = {}
  const response = await axios.post('https://api.blockset.com/clients', body, { headers })
  return response.data.token
}

Step 3. Generate a User JWT

Once you have a Client JWT, you need to inform the Blockset API of your public key, which is device-specific.

To prove to Blockset that you control the private key associated with your public key, you can sign a message using that private key. The message you sign should include the Client JWT that Blockset just gave you, to associate your device with the Client.

We'll send that information back to Blockset, and Blockset will respond back with some more server-generated tokens that we can use during the construction of our User JWT.

Finally, we'll locally create that User JWT.

Step 3a. Create a Public/Private Keypair

We can use open source libraries (bitcore-lib and bitcore-mnemonic) to generate a keypair.

const bitcore = require('bitcore-lib')
const Mnemonic = require('bitcore-mnemonic')

const makeKeypair = () => {
  const code = new Mnemonic(Mnemonic.Words.ENGLISH)
  const xpriv = new bitcore.HDPrivateKey(code.toHDPrivateKey().toString())
  const keypair = xpriv.deriveChild("m/1'/0")
  return { privateKey: keypair.privateKey, publicKey: keypair.publicKey }
}

Step 3b. Sign the User Token Request

Now let's actually sign the Client JWT that Blockset gave us, using our private key. We'll return back an object containing the signature, our public key, and a randomly generated device ID.

We'll use bitcore-lib again, and another open-source library for generating a random device ID, called uuid.

const bitcore = require('bitcore-lib')
const { v4: uuidv4 } = require('uuid')

const signUserTokenRequest = (clientJwt, privateKey, publicKey) => {
  const clientJwtHash = bitcore.crypto.Hash.sha256(Buffer.from(clientJwt, 'utf-8'))
  const clientJwtSignature = bitcore.crypto.ECDSA.sign(clientTokenHash, privateKey)
  
  return {
    signature: clientTokenSignature.toDER().toString('base64'),
    publicKey: publicKey.toDER().toString('base64'),
    deviceId: uuidv4()
  }
}

Step 3c. Get the User Token Request's Response

Finally, we'll use the User Token Request to inform the Blockset API of our public key, so that we can generate our own User JWT that the Blockset API can validate independently, without knowing our password!

const getUserTokenRequest = async (signature, publicKey, deviceId, clientJwt) => {
  const headers = { authorization: `Bearer ${clientJwt}` }
  const body = { signature, pub_key: publicKey, device_id: deviceId }
  const response = await axios.post('https://api.blockset.com/users/token', body, { headers })
  return { userToken: response.data.token, clientToken: response.data.client_token }
}

Step 3d. Generate the User JWT

Now we have all of the information we need to generate a User JWT locally, which we can use as the Authentication token for our future calls to the Blockset API!

We'll use an open-source library called key-encoder to encode our private key into a format readable by an open-source JWT generation library, jsonwebtoken.

const jsonwebtoken = require('jsonwebtoken')
const KeyEncoder = require('key-encoder').default
const keyEncoder = new KeyEncoder('secp256k1')

const makeUserJwt = (userToken, clientToken, privateKey) => {
  const body = { 'brd:ct': 'usr', 'brd:cli': clientToken }
  const pemPrivateKey = keyEncoder.encodePrivate(privateKey.toString(), 'raw', 'pem')
  const options = { algorithm: 'ES256', subject: userToken, expiresIn: '1y' }
  const response = jsonwebtoken.sign(body, pemPrivateKey, options)
  return response
}

Congrats! Now you have a User JWT that can be used to uniquely authenticate a specific device against the Blockset API.

Putting it All Together

Let's see what this code looks like when combining it all together.

const axios = require('axios')
const bitcore = require('bitcore-lib')
const Mnemonic = require('bitcore-mnemonic')
const jsonwebtoken = require('jsonwebtoken')
const KeyEncoder = require('key-encoder').default
const keyEncoder = new KeyEncoder('secp256k1')
const { v4: uuidv4 } = require('uuid')

const getAccountJwt = async (email, password) => {
  const body = { email, password }
  const response = await axios.post('https://api.blockset.com/accounts/login', body)
  return response.data.token
}

const getClientJwt = async (accountJwt) => {
  const headers = { authorization: `Bearer ${accountJwt}` }
  const body = {}
  const response = await axios.post('https://api.blockset.com/clients', body, { headers })
  return response.data.token
}

const makeKeypair = () => {
  const code = new Mnemonic(Mnemonic.Words.ENGLISH)
  const xpriv = new bitcore.HDPrivateKey(code.toHDPrivateKey().toString())
  const keypair = xpriv.deriveChild("m/1'/0")
  return { privateKey: keypair.privateKey, publicKey: keypair.publicKey }
}

const signUserTokenRequest = (clientJwt, privateKey, publicKey) => {
  const clientJwtHash = bitcore.crypto.Hash.sha256(Buffer.from(clientJwt, 'utf-8'))
  const clientJwtSignature = bitcore.crypto.ECDSA.sign(clientTokenHash, privateKey)
  
  return {
    signature: clientTokenSignature.toDER().toString('base64'),
    publicKey: publicKey.toDER().toString('base64'),
    deviceId: uuidv4()
  }
}

const getUserTokenRequest = async (signature, publicKey, deviceId, clientJwt) => {
  const headers = { authorization: `Bearer ${clientJwt}` }
  const body = { signature, pub_key: publicKey, device_id: deviceId }
  const response = await axios.post('https://api.blockset.com/users/token', body, { headers })
  return { userToken: response.data.token, clientToken: response.data.client_token }
}

const makeUserJwt = (userToken, clientToken, privateKey) => {
  const body = { 'brd:ct': 'usr', 'brd:cli': clientToken }
  const pemPrivateKey = keyEncoder.encodePrivate(privateKey.toString(), 'raw', 'pem')
  const options = { algorithm: 'ES256', subject: userToken, expiresIn: '1y' }
  const response = jsonwebtoken.sign(body, pemPrivateKey, options)
  return response
}

const makeTokens = async () => {
  const accountJwt = await getAccountJwt('[email protected]', 'i fixed the double-spend problem')
  const clientJwt = await getClientJwt(accountJwt)
  const keypair = makeKeypair()
  const signedRequest = signUserTokenRequest(clientJwt, keypair.privateKey, keypair.publicKey)
  const userJwtRequest = getUserTokenRequest(signedRequest.signature, signedRequest.publicKey, signedRequest.deviceId, clientJwt)
  const userJwt = makeUserJwt(userJwtRequest.userToken, userJwtRequest.clientToken, keypair.privateKey)

  return ({ accountJwt, clientJwt, userJwt })
}

console.log(makeTokens())

Ready to move on? Head over to Hello, World to begin implementing Blockset.

Need help? Contact us
Blockset both powers, and is brought to you by BRD , the world’s oldest and most trusted mobile wallet.