Advanced API Key

Our Advanced API Key uses JSON Web Token (JWT), which is an open standard for securely exchanging data between parties using the JSON format. For our API authentication purposes we require the use of public/private key pair either ECDSA or RSA to sign the JWT. For each authenticated request the header must contain the signature of the token signed by the private key of the key pair, and we will verify the signature using the public key of the key pair.

Key pair generation and pairing

Use one of the many libraries to generate a public private key pair using either RSA or ECDSA. The supported algorithms are:

  • RSA
    • RS256
    • RS384
    • RS512
  • ECDSA
    • ES256
    • ES384
    • ES512
    • ES256K

Using the jose npm package here is an example:

import * as jose from 'jose';
import * as fs from 'fs';

async function generateKeyPair() {
  const { publicKey, privateKey } = await jose.generateKeyPair('ES256');

  const publicJwk = await jose.exportJWK(publicKey);
  const privateJwk = await jose.exportJWK(privateKey);

  fs.writeFileSync('public_key.json', JSON.stringify(publicJwk, null, 2));
  fs.writeFileSync('private_key.json', JSON.stringify(privateJwk, null, 2));
}

generateKeyPair();

The private_key.json which you must treat as a password and kept safe and private. It will look something like:

{
  "kty": "EC",
  "x": "10lHl5RJF5S-bSLBFJ_tx_TZZ8TSMbQmAV5qP6PpJbA",
  "y": "3zmIen2m-rsXoGg6P_Qw-VbHh2_B6dfNiJHwQU3HrW8",
  "crv": "P-256",
  "d": "X_lZJjWU4ystK61y4pzojVUV9HYy1KFprAP9_LLnuOE"
}

The publickey.json you will need to upload to CoinJar Exchange in the following steps looks like below. Note that it is _very similar to the private_key.json so be careful.

{
  "kty": "EC",
  "x": "10lHl5RJF5S-bSLBFJ_tx_TZZ8TSMbQmAV5qP6PpJbA",
  "y": "3zmIen2m-rsXoGg6P_Qw-VbHh2_B6dfNiJHwQU3HrW8",
  "crv": "P-256"
}

Once you have both the public and private key json files you can start the key creation flow in the UI, which will involve copy and pasting the public key into a form in the UI before submission, this is referred to as the 'key pairing'. Once the key is paired successfully you will receive a kid which looks something like e03f773e-5203-4f39-a5f2-45bfd3ea48ck, and is used to uniquely identify the api key. For all JWTs in authenticated requests this kid must be included in the payload.

Signing JWT

A valid JWT can be signed with the following parameters as header:

alg: ES256, ES384, ES512, ES256K, RSA256, RSA384, RSA512 (corresponding to the curve used in your JWK)
kid: Key ID assigned by CoinJar upon successful key pairing
typ: JWT

The following parameters must be used as payload:

aud: CJX
iat: Issued time of the token in Unix timestamp (seconds)
exp: Expiration time of the token in Unix timestamp (seconds)

Below is an example using npm package jose to sign a payload and header:

import * as jose from 'jose';
import * as fs from 'fs';

export async function signJWT(scope:string = 'read') {
  const privateKeyJson = JSON.parse(fs.readFileSync('private_key.json', 'utf-8'));
  const privateKey = await jose.importJWK(privateKeyJson, 'ES256');

  const payload: jose.JWTPayload = {
    aud: 'CJX',
    scope,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600,
  };

  const header: jose.JWTHeaderParameters  = {
    alg: 'ES256',
    kid: '7e940191-d068-4a6e-9c83-e2127b5641ed',
    typ: 'JWT'
  }

  const jwt:string = await new jose.SignJWT(payload)
    .setProtectedHeader(header)
    .sign(privateKey);

  return jwt;
}

Using the signed JWT

Once a signature of the JWT is created it needs to be included in the Authorization header of the http request just like the classic api key, Authorization: Bear ${signature}. Below is an example following on from above using signJWT().

async function getAccounts() {
  const baseUrl = 'https://api.exchange.coinjar-sandbox.com'
  const signature = await signJWT('read');
  const response = await fetch(`${baseUrl}/accounts`, {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${signature}`
    }
  })
  const json = await response.json()
	return json
}

async function createOrder() {
  const baseUrl = 'https://api.exchange.coinjar-sandbox.com'
  const signature = await signJWT('trade');
  const response = await fetch(`${baseUrl}/orders`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${signature}`
    },
    body: JSON.stringify({
      type: 'LMT',
      side: 'buy',
      price: '100000',
      size: '0.001',
      product_id: 'BTCAUD',
    })
  })
  const json = await response.json()
  return json
}

Validity constraints and timing

A JWT can be re-used for multiple requests, as long as current time is at or before the exp of the token. Best security practices suggest the use of short-lived tokens, and CoinJar Exchange API limits the maximum validity of JWTs.

In production environment, JWTs must not have a validity over more than 3,600 seconds (1 hour). In sandbox, this limit is increased to 86,400 seconds (24 hours) to provide a better development experience. There is no minimum validity requirement, so if your workflow is capable of using very short-lived tokens (e.g. 60 seconds), it's highly recommended.

Having an accurate clock is important to expire and refresh tokens correctly. We recommend using NTP on all servers for time synchronisation, and refresh tokens at least 5 seconds ahead of the expiration to allow for minor time differences between systems.