Using JSON Web Tokens (JWTs)

To authenticate to Omnicore, each device must prepare a JSON Web Token (JWT, RFC 7519). JWTs are used for short-lived authentication between devices and the MQTT or HTTP bridges. This page describes the Omnicore requirements for the contents of the JWT.

Omnicore does not require a specific token generation method. A good collection of helper client libraries can be found on JWT.io.

When creating an MQTT client, the JWT must be passed in the password field of the CONNECT message. When connecting over HTTP, a JWT must be included as bearer token in the 'Authorization' header of each HTTP request.

Mqtt Connection Field KeyMqtt Connection Field Value

Client ID

subscriptions/{subscription-id}/registries/{registry-id}/devices/{device-id}

Host

hostprefix.mqtt.korewireless.com

Port

8883

Username

unused

Password

JWT Token

SSL/TLS

True

Certificate

Available

SSL Secure

On

Creating JWTs

JWTs are composed of three sections: a header, a payload (containing a claim set), and a signature. The header and payload are JSON objects, which are serialized to UTF-8 bytes, then encoded using base64url encoding.

The JWT's header, payload, and signature are concatenated with periods (.). As a result, a JWT typically takes the following form:

{Base64url encoded header}.{Base64url encoded payload}.{Base64url encoded signature}

The following sample illustrates how to create a Omnicore JWT . After creating the JWT, you can connect to the MQTT or HTTP bridge to publish messages from a device. The steps for configuring the client ID and authenticating a device.

/**
 * Calculates issued at / expiration times for JWT and places the time, as a
 * Unix timestamp, in the strings passed to the function. The time_size
 * parameter specifies the length of the string allocated for both iat and exp.
 */
static void GetIatExp(char* iat, char* exp, int time_size) {
  // TODO(#72): Use time.google.com for iat
  time_t now_seconds = time(NULL);
  snprintf(iat, time_size, "%lu", now_seconds);
  snprintf(exp, time_size, "%lu", now_seconds + 3600);
  if (TRACE) {
    printf("IAT: %s\n", iat);
    printf("EXP: %s\n", exp);
  }
}

static int GetAlgorithmFromString(const char* algorithm) {
  if (strcmp(algorithm, "RS256") == 0) {
    return JWT_ALG_RS256;
  }
  if (strcmp(algorithm, "ES256") == 0) {
    return JWT_ALG_ES256;
  }
  return -1;
}

/**
 * Calculates a JSON Web Token (JWT) given the path to a EC private key .
 *  Returns the JWT as a string that the caller must
 * free.
 */
static char* CreateJwt(const char* ec_private_path,
                       const char* algorithm) {
  char iat_time[sizeof(time_t) * 3 + 2];
  char exp_time[sizeof(time_t) * 3 + 2];
  uint8_t* key = NULL;  // Stores the Base64 encoded certificate
  size_t key_len = 0;
  jwt_t* jwt = NULL;
  int ret = 0;
  char* out = NULL;

  // Read private key from file
  FILE* fp = fopen(ec_private_path, "r");
  if (fp == NULL) {
    printf("Could not open file: %s\n", ec_private_path);
    return "";
  }
  fseek(fp, 0L, SEEK_END);
  key_len = ftell(fp);
  fseek(fp, 0L, SEEK_SET);
  key = malloc(sizeof(uint8_t) * (key_len + 1));  // certificate length + \0

  fread(key, 1, key_len, fp);
  key[key_len] = '\0';
  fclose(fp);

  // Get JWT parts
  GetIatExp(iat_time, exp_time, sizeof(iat_time));

  jwt_new(&jwt);

  // Write JWT
  ret = jwt_add_grant(jwt, "iat", iat_time);
  if (ret) {
    printf("Error setting issue timestamp: %d\n", ret);
  }
  ret = jwt_add_grant(jwt, "exp", exp_time);
  if (ret) {
    printf("Error setting expiration: %d\n", ret);
  }
  ret = jwt_set_alg(jwt, GetAlgorithmFromString(algorithm), key, key_len);
  if (ret) {
    printf("Error during set alg: %d\n", ret);
  }
  out = jwt_encode_str(jwt);
  if (!out) {
    perror("Error during token creation:");
  }
  // Print JWT
  if (TRACE) {
    printf("JWT: [%s]\n", out);
  }

  jwt_free(jwt);
  free(key);
  return out;
}

JWT header

The JWT header consists of two fields that indicate the signing algorithm and the type of token. Both fields are mandatory, and each field has only one value. Omnicore supports the following signing algorithms:

  • JWT RS256 (RSASSA-PKCS1-v1_5 using SHA-256 RFC 7518 sec 3.3). This is expressed as RS256 in the alg field in the JWT header.

  • JWT ES256 (ECDSA using P-256 and SHA-256 RFC 7518 sec 3.4), defined in OpenSSL as the prime256v1 curve. This is expressed as ES256 in the alg field in the JWT header.

In addition to the signing algorithm, you must supply the JWT token format.

The JSON representation of the header is as follows:

For RSA keys:

{ "alg": "RS256", "typ": "JWT" }

For Elliptic Curve keys:

{ "alg": "ES256", "typ": "JWT" }

The algorithm specified in the header must match at least one of the public keys registered for the device.

NOTE

The JWT header is not the same as the HTTP header (if you're connecting over HTTP).

JWT claims

The JWT payload contains a set of claims, and it is signed using the asymmetric keys. The JWT claim set contains information about the JWT, such as the target of the token, the issuer, the time the token was issued, and/or the lifetime of the token. Like the JWT header, the JWT claim set is a JSON object and is used in the calculation of the signature.

Required claims

Omnicore requires the following reserved claim fields. They may appear in any order in the claim set.

NameDescription

iat

("Issued At"): The timestamp when the token was created, specified as seconds since 00:00:00 UTC, January 1, 1970. The server may report an error if this timestamp is too far in the past or the future (allowing 10 minutes for skew).

exp

("Expiration"): The timestamp when the token stops being valid, specified as seconds since 00:00:00 UTC, January 1, 1970. The maximum lifetime of a token is 24 hours + skew. All MQTT connections will be closed by the server a few seconds after the token expires (allowing for skew), because MQTT does not have a way to refresh credentials. A new token must be minted to reconnect. Note that because of the allowed skew, in practice the minimum lifetime of a token will be be equal to the acceptable clock skew, even if it is set to one second. When connecting over HTTP, each HTTP request must include a JWT, regardless of expiration time.

The nbf("Not Before") claim will be ignored, and is not required.

A JSON representation of the required reserved fields in a Omnicore JWT claim set is shown below:

For Elliptic Curve keys:

{
  "iat": 1509654401,
  "exp": 1612893233
}

JWT signature

The JSON Web Signature (JWS) specification guides the mechanics of generating the signature for the JWT. The input for the signature is the byte array of the following content:

{Base64url encoded header}.{Base64url encoded claim set}

To compute the signature, sign the base64url-encoded header, base64-url encoded claim set, and a secret key (such as an rsa_private.pem file) using the algorithm you defined in the header. The signature is then base64url-encoded, and the result is the JWT. The following example shows a JWT before base64url encoding:

{"alg": "RS256", "typ": "JWT"}.{ "iat": 1509654401, "exp": 1612893233}.[signature bytes]

After the final encoding, the JWT looks like the following:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjMxMDcwMjE5MDcsImlhdCI6MTY3MzU5Nzk0NH0.DNPWPXg8whGF66gycOJwnGcGv4bywtSt7GZEgeHrdrG_qJfIBNaYeeM1ElCy9bz9zw5X6qrXg-xdUsPTMgHCID3kiaZOu7yzdg4KXWIWQGAeWPUmeNXuXopvEvPu-398VDBuqXINTgf9O3WUBdzxHCW2iVOIJKvq7xybMZhcJmt_LEqlwGAM-xwE2-MSrnhnseLRkpIL_PH3YcHkfeb-0961XROFr-f5y3WEy8cyObt67iB_bO_QgShf0HQZwD6GFq-00D_HN7wdGYF4ruokV0SGLl-I7TkSqGdVbtLmDx38vXtF_S3ANegVNsu4pusvIHzXAcQ6MjOCuoYKNi8WjA

Last updated