Using JSON Web Tokens (JWTs)

To authenticate to Cloud IoT Core, each device must prepare a JSON Web Token (JWT, RFC 7519). JWTs provide a secure signing mechanism that verifies and sends information between your devices and Cloud IoT Core. This page describes the Cloud IoT Core requirements for the contents of the JWT.

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

When connecting over MQTT, the JWT must be set in the password field of the CONNECT message. When connecting over HTTP, a JWT must be included in the header of each HTTP request.

The following samples illustrate how to create a Cloud IoT Core JWT for a given project, signed with the given private key. For details, continue reading.

C/C++

/**
 * 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: 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);
  }
}

/**
 * Calculates a JSON Web Token (JWT) given the path to a EC private key and
 * Google Cloud project ID. Returns the JWT as a string that the caller must
 * free.
 */
static char* CreateJwt(const char* ec_private_path, const char* project_id) {
  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 == (void*) 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", ret);
  }
  ret = jwt_add_grant(jwt, "exp", exp_time);
  if (ret) {
    printf("Error setting expiration: %d", ret);
  }
  ret = jwt_add_grant(jwt, "aud", project_id);
  if (ret) {
    printf("Error adding audience: %d", ret);
  }
  ret = jwt_set_alg(jwt, JWT_ALG_ES256, key, key_len);
  if (ret) {
    printf("Error during set alg: %d", ret);
  }
  out = jwt_encode_str(jwt);

  // Print JWT
  if (TRACE) {
    printf("JWT: [%s]", out);
  }

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

Java

/** Create a Cloud IoT Core JWT for the given project id, signed with the given RSA key. */
private static String createJwtRsa(String projectId, String privateKeyFile) throws Exception {
  DateTime now = new DateTime();
  // Create a JWT to authenticate this device. The device will be disconnected after the token
  // expires, and will have to reconnect with a new token. The audience field should always be set
  // to the GCP project id.
  JwtBuilder jwtBuilder =
      Jwts.builder()
          .setIssuedAt(now.toDate())
          .setExpiration(now.plusMinutes(20).toDate())
          .setAudience(projectId);

  byte[] keyBytes = Files.readAllBytes(Paths.get(privateKeyFile));
  PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
  KeyFactory kf = KeyFactory.getInstance("RSA");

  return jwtBuilder.signWith(SignatureAlgorithm.RS256, kf.generatePrivate(spec)).compact();
}

/** Create a Cloud IoT Core JWT for the given project id, signed with the given ES key. */
private static String createJwtEs(String projectId, String privateKeyFile) throws Exception {
  DateTime now = new DateTime();
  // Create a JWT to authenticate this device. The device will be disconnected after the token
  // expires, and will have to reconnect with a new token. The audience field should always be set
  // to the GCP project id.
  JwtBuilder jwtBuilder =
      Jwts.builder()
          .setIssuedAt(now.toDate())
          .setExpiration(now.plusMinutes(20).toDate())
          .setAudience(projectId);

  byte[] keyBytes = Files.readAllBytes(Paths.get(privateKeyFile));
  PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
  KeyFactory kf = KeyFactory.getInstance("EC");

  return jwtBuilder.signWith(SignatureAlgorithm.ES256, kf.generatePrivate(spec)).compact();
}

Node.js

function createJwt (projectId, privateKeyFile, algorithm) {
  // Create a JWT to authenticate this device. The device will be disconnected
  // after the token expires, and will have to reconnect with a new token. The
  // audience field should always be set to the GCP project id.
  const token = {
    'iat': parseInt(Date.now() / 1000),
    'exp': parseInt(Date.now() / 1000) + 20 * 60, // 20 minutes
    'aud': projectId
  };
  const privateKey = fs.readFileSync(privateKeyFile);
  return jwt.sign(token, privateKey, { algorithm: algorithm });
}

Python

def create_jwt(project_id, private_key_file, algorithm):
    """Creates a JWT (https://jwt.io) to establish an MQTT connection.
        Args:
         project_id: The cloud project ID this device belongs to
         private_key_file: A path to a file containing either an RSA256 or
                 ES256 private key.
         algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256'
        Returns:
            An MQTT generated from the given project_id and private key, which
            expires in 20 minutes. After 20 minutes, your client will be
            disconnected, and a new JWT will have to be generated.
        Raises:
            ValueError: If the private_key_file does not contain a known key.
        """

    token = {
            # The time that the token was issued at
            'iat': datetime.datetime.utcnow(),
            # The time the token expires.
            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60),
            # The audience field should always be set to the GCP project id.
            'aud': project_id
    }

    # Read the private key file.
    with open(private_key_file, 'r') as f:
        private_key = f.read()

    print('Creating JWT using {} from private key file {}'.format(
            algorithm, private_key_file))

    return jwt.encode(token, private_key, algorithm=algorithm)

JWT composition

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}

JWT header

The 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. Cloud IoT Core 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.

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

Cloud IoT Core requires the following reserved claim fields. They may appear in any order in the claim set.

Name Description
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.
  • Note that clients in Network Time Protocol (NTP)-capable devices can use the Google Public NTP Server to keep the device clock synchronized; the requirement for authentication is to keep the clock synchronized with a skew of up to 10 minutes.
aud ("Audience"): This must be a single string containing the cloud project ID where the device is registered. If the connection request does not match this project ID, the authentication will be denied without further analysis.

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

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

{
  "aud": "my-project",
  "iat": 1509650801,
  "exp": 1509654401
}

Computing the 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"}.{"aud": "my-project", "iat": 1509650801, "exp": 1509654401}.[signature bytes]

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

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJteS1wcm9qZWN0IiwiZXhwIjoxNTA5NjUwODAxLCJpYXQiOjE1MDk2NTQ0MDF9.F4iKO0R0wvHkpCcQoyrYttdGxE5FLAgDhbTJQLEHIBPsbL2WkLxXB9IGbDESn9rE7oxn89PJFRtcLn7kJwvdQkQcsPxn2RQorvDAnvAi1w3k8gpxYWo2DYJlnsi7mxXDqSUCNm1UCLRCW68ssYJxYLSg7B1xGMgDADGyYPaIx1EdN4dDbh-WeDyLLa7a8iWVBXdbmy1H3fEuiAyxiZpk2ll7DcQ6ryyMrU2XadwEr9PDqbLe5SrlaJsQbFi8RIdlQJSo_DZGOoAlA5bYTDYXb-skm7qvoaH5uMtOUb0rjijYuuxhNZvZDaBerEaxgmmlO0nQgtn12KVKjmKlisG79Q

Refreshing JWTs

As described in required claims, tokens have expiration dates. If a device is connected over MQTT and its token expires, the device automatically disconnects from Cloud IoT Core. You can prevent the device from disconnecting by automatically refreshing its token. The following samples illustrate how to check whether a token has expired and, if it has, how to reconnect with a new token without disconnecting the device.

Java

long secsSinceRefresh = ((new DateTime()).getMillis() - iat.getMillis()) / 1000;
if (secsSinceRefresh > (options.tokenExpMins * 60)) {
  System.out.format("\tRefreshing token after: %d seconds\n", secsSinceRefresh);
  iat = new DateTime();
  if (options.algorithm.equals("RS256")) {
    connectOptions.setPassword(
        createJwtRsa(options.projectId, options.privateKeyFile).toCharArray());
  } else if (options.algorithm.equals("ES256")) {
    connectOptions.setPassword(
        createJwtEs(options.projectId, options.privateKeyFile).toCharArray());
  } else {
    throw new IllegalArgumentException(
        "Invalid algorithm " + options.algorithm
            + ". Should be one of 'RS256' or 'ES256'.");
  }
  client.disconnect();
  client.connect();
  attachCallback(client, options.deviceId);
}

Node.js

let secsFromIssue = parseInt(Date.now() / 1000) - iatTime;
if (secsFromIssue > argv.tokenExpMins * 60) {
  iatTime = parseInt(Date.now() / 1000);
  console.log(`\tRefreshing token after ${secsFromIssue} seconds.`);

  client.end();
  connectionArgs.password = createJwt(argv.projectId, argv.privateKeyFile, argv.algorithm);
  client = mqtt.connect(connectionArgs);

  client.on('connect', (success) => {
    console.log('connect');
    if (!success) {
      console.log('Client not connected...');
    } else if (!publishChainInProgress) {
      publishAsync(1, argv.numMessages);
    }
  });

  client.on('close', () => {
    console.log('close');
    shouldBackoff = true;
  });

  client.on('error', (err) => {
    console.log('error', err);
  });

  client.on('message', (topic, message, packet) => {
    console.log('message received: ', Buffer.from(message, 'base64').toString('ascii'));
  });

  client.on('packetsend', () => {
    // Note: logging packet send is very verbose
  });
}

Python

seconds_since_issue = (datetime.datetime.utcnow() - jwt_iat).seconds
if seconds_since_issue > 60 * jwt_exp_mins:
    print('Refreshing token after {}s').format(seconds_since_issue)
    jwt_iat = datetime.datetime.utcnow()
    client = get_client(
        args.project_id, args.cloud_region,
        args.registry_id, args.device_id, args.private_key_file,
        args.algorithm, args.ca_certs, args.mqtt_bridge_hostname,
        args.mqtt_bridge_port)

Was this page helpful? Let us know how we did:

Send feedback about...

Google Cloud Internet of Things Core