Google Cloud IoT Core ne sera plus disponible à compter du 16 août 2023. Pour en savoir plus, contactez l'équipe chargée de votre compte Google Cloud.

Utiliser des jetons Web JSON (JWT)

Pour s'authentifier auprès de Cloud IoT Core, chaque appareil doit préparer un jeton Web JSON (JWT, RFC 7519). Les jetons JWT sont utilisés pour l'authentification à court terme entre les appareils et les ponts MQTT ou HTTP. Cette page décrit les exigences pour Cloud IoT Core concernant le contenu du jeton JWT.

Cloud IoT Core ne nécessite pas de méthode de génération de jetons spécifique. Une bonne collection de bibliothèques clientes d'assistance est disponible sur JWT.io.

Lors de la création d'un client MQTT, le JWT doit être transmis dans le champ password du message CONNECT. Lors de la connexion via HTTP, un jeton JWT doit être inclus dans l'en-tête de chaque requête HTTP.

Créer des jetons JWT

Les jetons JWT comportent trois sections: un en-tête, une charge utile (contenant un ensemble de revendications) et une signature. L'en-tête et la charge utile sont des objets JSON, qui sont sérialisés en UTF-8 octets, puis encodés en base64url.

L'en-tête, la charge utile et la signature du jeton JWT sont concaténés avec des points (.). Par conséquent, un jeton JWT se présente généralement sous la forme suivante :

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

L'exemple suivant montre comment créer un JWT Cloud IoT Core pour un projet donné. Après avoir créé le JWT, vous pouvez vous connecter au pont MQTT ou HTTP pour publier des messages depuis un appareil.

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 {
  Instant now = Instant.now();
  // 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(Date.from(now))
          .setExpiration(Date.from(now.plusSeconds(20 * 60)))
          .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) 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)

En-tête JWT

L'en-tête JWT comprend deux champs qui indiquent l'algorithme de signature et le type de jeton. Ces deux champs sont obligatoires et chaque champ ne comporte qu'une seule valeur. Cloud IoT Core est compatible avec les algorithmes de signature suivants:

  • JWT RS256 (RSASSA-PKCS1-v1_5 avec SHA-256 RFC 7518 sec 3.3). Cela est exprimé au format RS256 dans le champ alg de l'en-tête JWT.
  • JWT ES256 (ECDSA à l'aide de P-256 et SHA-256 RFC 7518 s 3.4), défini dans OpenSSL comme courbe prime256v1. Cela est exprimé au format ES256 dans le champ alg de l'en-tête JWT.

En plus de l'algorithme de signature, vous devez fournir le format de jeton JWT.

La représentation JSON de l'en-tête est la suivante :

Pour les clés RSA :

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

Pour les clés à courbe elliptique: 

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

L'algorithme spécifié dans l'en-tête doit correspondre à au moins une des clés publiques enregistrées pour l'appareil.

Revendications JWT

La charge utile du jeton JWT contient un ensemble de revendications. Elle est signée à l'aide des clés asymétriques. L'ensemble de revendications JWT contient des informations sur le jeton JWT, telles que la cible du jeton, l'émetteur, l'heure d'émission du jeton et/ou sa durée de vie. Comme l'en-tête JWT, l'ensemble de revendications JWT est un objet JSON et est utilisé dans le calcul de la signature.

Revendications obligatoires

Cloud IoT Core nécessite les champs de revendication réservés suivants. Ils peuvent apparaître dans n'importe quel ordre dans l'ensemble de revendications.

Nom Description
iat ("Émis à"): horodatage de la création du jeton, exprimé en secondes depuis le 1er janvier 1970 à 00:00:00 UTC. Le serveur peut signaler une erreur si cet horodatage est trop antérieur dans le passé ou à l'avenir (en tenant compte d'un décalage de 10 minutes).
exp ("Expiration"): horodatage de la validité du jeton, exprimé en secondes depuis le 1er janvier 1970 à 00:00:00 UTC. La durée de vie maximale d'un jeton est de 24 heures avec décalage.
  • Toutes les connexions MQTT sont fermées par le serveur quelques secondes après l'expiration du jeton (ce qui permet un décalage), car MQTT ne peut pas actualiser les identifiants. Vous devrez frapper un nouveau jeton pour vous reconnecter. Notez qu'en raison du décalage autorisé, en pratique, la durée de vie minimale d'un jeton sera égale au décalage d'horloge acceptable, même s'il est défini sur une seconde.
  • Lors de la connexion HTTP, chaque requête HTTP doit inclure un jeton JWT, quel que soit le délai d'expiration.
  • Notez que les clients des appareils compatibles avec le protocole NTP (Network Time Protocol) peuvent utiliser le serveur NTP public de Google pour maintenir l'horloge des appareils synchronisée. La condition d'authentification est de maintenir l'horloge synchronisée avec un écart de 10 minutes.
aud ("Audience"): il doit s'agir d'une chaîne unique contenant l'ID du projet cloud où l'appareil est enregistré. Si la requête de connexion ne correspond pas à cet ID de projet, l'authentification sera refusée sans analyse supplémentaire.

La revendication nbf("Pas avant") sera ignorée et n'est pas obligatoire.

Une représentation JSON des champs réservés requis dans un ensemble de revendications JWT Cloud IoT Core est présentée ci-dessous :

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

Signature du jeton JWT

La spécification JSON Web Signature (JWS) guide la mécanique de génération de la signature pour le JWT. L'entrée de la signature correspond au tableau d'octets du contenu suivant:

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

Pour calculer la signature, signez l'en-tête encodé en base64url, l'ensemble de revendications encodées en base64 et une clé secrète (un fichier rsa_private.pem, par exemple) à l'aide de l'algorithme que vous avez défini dans l'en-tête. La signature est ensuite encodée en base64url et le résultat est le JWT. L'exemple suivant montre un JWT avant l'encodage base64url:

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

Après l'encodage final, le jeton JWT se présente comme suit :

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJteS1wcm9qZWN0IiwiZXhwIjoxNTA5NjUwODAxLCJpYXQiOjE1MDk2NTQ0MDF9.F4iKO0R0wvHkpCcQoyrYttdGxE5FLAgDhbTJQLEHIBPsbL2WkLxXB9IGbDESn9rE7oxn89PJFRtcLn7kJwvdQkQcsPxn2RQorvDAnvAi1w3k8gpxYWo2DYJlnsi7mxXDqSUCNm1UCLRCW68ssYJxYLSg7B1xGMgDADGyYPaIx1EdN4dDbh-WeDyLLa7a8iWVBXdbmy1H3fEuiAyxiZpk2ll7DcQ6ryyMrU2XadwEr9PDqbLe5SrlaJsQbFi8RIdlQJSo_DZGOoAlA5bYTDYXb-skm7qvoaH5uMtOUb0rjijYuuxhNZvZDaBerEaxgmmlO0nQgtn12KVKjmKlisG79Q

Actualiser les jetons JWT

Comme indiqué dans les revendications obligatoires, les jetons ont des dates d'expiration. Si un appareil est connecté via MQTT et que son jeton expire, il se déconnecte automatiquement de Cloud IoT Core. Vous pouvez empêcher la déconnexion de l'appareil en actualisant automatiquement son jeton. Les exemples suivants montrent comment vérifier si un jeton a expiré et, le cas échéant, comment se reconnecter avec un nouveau jeton sans déconnecter l'appareil.

Java

long secsSinceRefresh = (Instant.now().toEpochMilli() - iat.toEpochMilli()) / 1000;
if (secsSinceRefresh > (options.tokenExpMins * MINUTES_PER_HOUR)) {
  System.out.format("\tRefreshing token after: %d seconds%n", secsSinceRefresh);
  iat = Instant.now();
  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(f"Refreshing token after {seconds_since_issue}s")
    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,
    )