Como usar JSON Web Tokens (JWTs)

Para autenticar no Cloud IoT Core, cada dispositivo precisa preparar um JSON Web Token (JWT, RFC 7519). Os JWTs são usados para autenticação de curta duração entre dispositivos e as pontes MQTT ou HTTP. Nesta página, descrevemos os requisitos do Cloud IoT Core para o conteúdo do JWT.

O Cloud IoT Core não requer um método específico de geração de tokens. Uma coleção de bibliotecas de cliente auxiliares pode ser encontrada em JWT.io.

Ao criar um cliente MQTT, o JWT precisa ser transmitido no campo password da mensagem CONNECT. Durante a conexão por HTTP, um JWT precisa ser incluído no cabeçalho de cada solicitação HTTP.

Como criar JWTs

Os JWTs são compostos de três seções: um cabeçalho, um payload (contendo um conjunto de declarações) e uma assinatura. O cabeçalho e o payload são objetos JSON, que são serializados para bytes UTF-8 e codificados com codificação base64url.

O cabeçalho, o payload e a assinatura do JWT são concatenados com pontos (.). Como resultado, um JWT geralmente tem o seguinte formato:

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

Veja na amostra a seguir como criar um JWT do Cloud IoT Core para um determinado projeto. Após criar o JWT, você pode se conectar à ponte MQTT ou HTTP para publicar mensagens de um dispositivo.

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(#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 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,
                       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_add_grant(jwt, "aud", project_id);
  if (ret) {
    printf("Error adding audience: %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;
}

Go


import (
	"errors"
	"io/ioutil"
	"time"

	jwt "github.com/golang-jwt/jwt"
)

// createJWT creates a Cloud IoT Core JWT for the given project id.
// algorithm can be one of ["RS256", "ES256"].
func createJWT(projectID string, privateKeyPath string, algorithm string, expiration time.Duration) (string, error) {
	claims := jwt.StandardClaims{
		Audience:  projectID,
		IssuedAt:  time.Now().Unix(),
		ExpiresAt: time.Now().Add(expiration).Unix(),
	}

	keyBytes, err := ioutil.ReadFile(privateKeyPath)
	if err != nil {
		return "", err
	}

	token := jwt.NewWithClaims(jwt.GetSigningMethod(algorithm), claims)

	switch algorithm {
	case "RS256":
		privKey, _ := jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
		return token.SignedString(privKey)
	case "ES256":
		privKey, _ := jwt.ParseECPrivateKeyFromPEM(keyBytes)
		return token.SignedString(privKey)
	}

	return "", errors.New("Cannot find JWT algorithm. Specify 'ES256' or 'RS256'")
}

Java

static MqttCallback mCallback;
static long MINUTES_PER_HOUR = 60;

/** 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 NoSuchAlgorithmException, IOException, InvalidKeySpecException {
  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();
}

Node.js

const 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 = 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:
        A JWT 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.now(tz=datetime.timezone.utc),
        # The time the token expires.
        "exp": datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(minutes=20),
        # 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)

Cabeçalho JWT

O cabeçalho JWT consiste em dois campos que indicam o algoritmo de assinatura e o tipo de token. Ambos os campos são obrigatórios, e cada um tem apenas um valor. O Cloud IoT Core oferece suporte aos seguintes algoritmos de assinatura:

  • JWT RS256 (RSASSA-PKCS1-v1_5 usando SHA-256 RFC 7518 sec 3.3). Ele é expresso como RS256 no campo alg, no cabeçalho JWT.
  • JWT ES256 (ECDSA usando P-256 e SHA-256 RFC 7518 sec 3.4), definido no OpenSSL como a curva prime256v1. Ele é expresso como ES256 no campo alg, no cabeçalho JWT.

Além do algoritmo de assinatura, você precisa fornecer o formato de token JWT.

A representação JSON do cabeçalho é a seguinte:

Para chaves RSA:

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

Para chaves de curva elíptica:

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

O algoritmo especificado no cabeçalho precisa corresponder a pelo menos uma das chaves públicas registradas no dispositivo.

Declarações do JWT

O payload do JWT contém um conjunto de declarações e é assinado com as chaves assimétricas. O conjunto de declarações JWT contém informações sobre o JWT, como o destino do token, o emissor, o horário em que o token foi emitido e/ou o ciclo de vida dele. Assim como o cabeçalho JWT, o conjunto de declarações JWT é um objeto JSON e é usado no cálculo da assinatura.

Reivindicações obrigatórias

O Cloud IoT Core requer os seguintes campos de declaração reservados. Elas podem aparecer em qualquer ordem no conjunto de reivindicações.

Nome Descrição
iat ("Emitido em"): o carimbo de data/hora em que o token foi criado, especificado como segundos desde 00:00:00 UTC, 1o de janeiro de 1970. O servidor poderá informar um erro se esse carimbo de data/hora estiver muito distante no passado ou no futuro, permitindo um desvio de 10 minutos.
exp ("Validade"): o carimbo de data/hora em que o token deixa de ser válido, especificado como segundos desde 00:00:00 UTC, 1o de janeiro de 1970. A vida útil máxima de um token é de 24 horas + desvio.
  • Todas as conexões MQTT serão fechadas pelo servidor alguns segundos depois que o token expirar (permitindo o desvio), já que o MQTT não tem como atualizar as credenciais. É preciso que um novo token seja criado para se reconectar. Observe que, por causa do desvio permitido, na prática, o ciclo de vida mínimo de um token será igual ao desvio do relógio aceitável, mesmo que esteja definido para um segundo.
  • Durante a conexão por HTTP, cada solicitação HTTP precisa incluir um JWT, independentemente do prazo de validade.
  • Os clientes em dispositivos compatíveis com o Network Time Protocol (NTP) podem usar o Servidor NTP público do Google para manter o relógio do dispositivo sincronizado. o requisito de autenticação é manter o relógio sincronizado com um desvio de até 10 minutos.
aud ("Público"): precisa ser uma única string que contém o ID do projeto do Cloud em que o dispositivo está registrado. Se a solicitação de conexão não corresponder a este ID do projeto, a autenticação será negada sem análises adicionais.

A declaração nbf("Não antes") será ignorada e não será obrigatória.

Veja abaixo uma representação JSON dos campos reservados obrigatórios em um conjunto de declarações JWT do Cloud IoT Core:

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

Assinatura JWT

A especificação JSON Web Signature (JWS) orienta a mecânica de geração da assinatura para o JWT. A entrada da assinatura é a matriz de bytes do seguinte conteúdo:

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

Para calcular a assinatura, assine o cabeçalho codificado em base64url, o conjunto de declarações codificadas em base64-url e uma chave secreta (como um arquivo rsa_private.pem) usando o algoritmo que você definiu no cabeçalho. Em seguida, a assinatura é codificada em base64url, e o resultado é o JWT. O exemplo a seguir mostra um JWT antes da codificação em base64url:

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

Após a codificação final, o JWT se parece com o seguinte:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJteS1wcm9qZWN0IiwiZXhwIjoxNTA5NjUwODAxLCJpYXQiOjE1MDk2NTQ0MDF9.F4iKO0R0wvHkpCcQoyrYttdGxE5FLAgDhbTJQLEHIBPsbL2WkLxXB9IGbDESn9rE7oxn89PJFRtcLn7kJwvdQkQcsPxn2RQorvDAnvAi1w3k8gpxYWo2DYJlnsi7mxXDqSUCNm1UCLRCW68ssYJxYLSg7B1xGMgDADGyYPaIx1EdN4dDbh-WeDyLLa7a8iWVBXdbmy1H3fEuiAyxiZpk2ll7DcQ6ryyMrU2XadwEr9PDqbLe5SrlaJsQbFi8RIdlQJSo_DZGOoAlA5bYTDYXb-skm7qvoaH5uMtOUb0rjijYuuxhNZvZDaBerEaxgmmlO0nQgtn12KVKjmKlisG79Q

Como atualizar JWTs

Conforme descrito nas reivindicações obrigatórias, os tokens têm datas de validade. Se um dispositivo estiver conectado por MQTT e o token expirar, o dispositivo será desconectado automaticamente do Cloud IoT Core. É possível evitar que o dispositivo seja desconectado atualizando o token automaticamente. Os exemplos a seguir mostram como verificar se um token expirou e, em caso afirmativo, como se reconectar a um novo token sem desconectar o dispositivo.

Java

long secsSinceRefresh = ((new DateTime()).getMillis() - iat.getMillis()) / 1000;
if (secsSinceRefresh > (options.tokenExpMins * MINUTES_PER_HOUR)) {
  System.out.format("\tRefreshing token after: %d seconds%n", secsSinceRefresh);
  iat = new DateTime();
  if ("RS256".equals(options.algorithm)) {
    connectOptions.setPassword(
        createJwtRsa(options.projectId, options.privateKeyFile).toCharArray());
  } else if ("ES256".equals(options.algorithm)) {
    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(connectOptions);
  attachCallback(client, options.deviceId);
}

Node.js

const 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
  );
  connectionArgs.protocolId = 'MQTT';
  connectionArgs.protocolVersion = 4;
  connectionArgs.clean = true;
  client = mqtt.connect(connectionArgs);

Python

seconds_since_issue = (datetime.datetime.now(tz=datetime.timezone.utc) - jwt_iat).seconds
if seconds_since_issue > 60 * jwt_exp_mins:
    print("Refreshing token after {}s".format(seconds_since_issue))
    jwt_iat = datetime.datetime.now(tz=datetime.timezone.utc)
    client.loop()
    client.disconnect()
    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,
    )