Authentication and authorization

In short

Each API request is authenticated and verified using a custom signature scheme. The signature is a hashed message authentication code (HMAC) using SHA-256 as the cryptographic hash function.

What does that even mean?

The API user is provided with two keys, a public and a private one. The public key is only used to identify (not authenticate) the user, and it is included in the HTTP(S) requests in plain text. The private key, however, is much like a password, and it is never transmitted alongside the requests. The private key is only used for key-hashing the signature on the server side.

The signature is a keyed hash of the public key, a timestamp of when the request was made, and the query string itself. The keyed hash is generated on the client side using the private key, with SHA-256 as the cryptographic hash function. The same strings are included in plain text in the HTTP(S) request. The hashing process is repeated with the same information on the server side, using the private key that matches the given public key.

If the signature matches, we can be sure that all the information is correct:

  • Identity - the request is coming from someone who knows both the public and private keys
  • Time - the request cannot be reused by an attacker at a later time
  • Content - the call (including any sent data) itself cannot have been tampered with along the way

Signature

The HTTP request should include an Authorization header in the following format:

Authorization: LYYTI-API-V2 public_key=aaaaa, timestamp=bbbbb, signature=ccccc

Name

Field title

public_key

The public API key, which is only used for identifying the API user. It is useless without knowing the private key.

timestamp

Standard Unix epoch timestamp. UTC.

signature

A signature used for both the authentication as well as authorization of the API key user.

A valid signature is a keyed SHA-256 hash (HMAC) of the same fields as are included in the HTTP Authorization header, with the addition of the API call string (eg. "events/123"). NOTE: The call string is expected to be everything following the API base URL including the last slash, so be sure to trim off the leading slash from the call string! The fields are concatenated into a comma-separated string, encoded with MIME Base64, and then hashed using the user's private API key.

To test your implementation, for

  • public key "vv8y2oro0f112moygbwnelzg3hzucfw8"
  • private key "w78b4xjp1id8lat5j69qry7ilqf63vt6"
  • timestamp seconds 1620124127
  • call string "events/123?query1=value1&query2=value2"

the Authorization header should be exactly Authorization: LYYTI-API-V2 public_key=vv8y2oro0f112moygbwnelzg3hzucfw8, timestamp=1620124127, signature=4c2093ed3127ce1b0dae9ba3d265f98ac810b7718865641d7bfd76f2215ec903 and the URL should be https://api.lyyti.com/v2/events/123?query1=value1&query2=value2.

The above will not actually work in the API and is just for testing Authorization header generation.

Below are actual example implementations in popular languages.

<?php

define('API_ROOT', 'https://api.lyyti.com/v2/');

$public_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$private_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$call_string = 'events/<event_id>?query1=value1&query2=value2';
$timestamp_sec = time();

$msg = implode(',', [
    $public_key,
    $timestamp_sec,
    $call_string,
]);

$signature = hash_hmac(
    'sha256',
    base64_encode($msg),
    $private_key
);

$headers = [
    'Accept: application/json; charset=utf-8',
    sprintf('Authorization: LYYTI-API-V2 public_key=%s, timestamp=%s, signature=%s', $public_key, $timestamp_sec, $signature),
];

$ch = curl_init(API_ROOT . $call_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = json_decode(curl_exec($ch));
curl_close($ch);
#!/usr/bin/env python3

import hashlib
import hmac
import base64
import time
import urllib.request
import json

API_ROOT = 'https://api.lyyti.com/v2/'

public_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
private_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
call_string = 'events/<event_id>?query1=value1&query2=value2'
timestamp_sec = int(time.time())

msg = ','.join([
    public_key,
    str(timestamp_sec),
    call_string,
])

signature = hmac.new(
    private_key.encode('utf-8'),
    msg=base64.b64encode(msg.encode('utf-8')),
    digestmod=hashlib.sha256
).hexdigest()

headers = {
    'Accept': 'application/json; charset=utf-8',
    'Authorization': 'LYYTI-API-V2 public_key={}, timestamp={}, signature={}'.format(public_key, timestamp_sec, signature),
}

request = urllib.request.Request(API_ROOT + call_string, headers=headers)
response = urllib.request.urlopen(request)
data = json.loads(response.read())