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 de courte durée entre les appareils et les ponts MQTT ou HTTP. Cette page décrit les exigences relatives à Cloud IoT Core pour le contenu du jeton JWT.

Cloud IoT Core ne nécessite pas de méthode de génération de jetons spécifique. Vous trouverez un bon ensemble de bibliothèques clientes d'aide sur JWT.io.

Lors de la création d'un client MQTT, le jeton 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 sont composés de trois sections: une 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 octets UTF-8, 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 jeton JWT Cloud IoT Core pour un projet donné. Après avoir créé le jeton JWT, vous pouvez vous connecter au pont MQTT ou HTTP pour publier des messages à partir d'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 {
  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)

En-tête JWT

L'en-tête JWT comprend deux champs indiquant l'algorithme de signature et le type de jeton. Ces deux champs sont obligatoires et ne doivent contenir qu'une seule valeur. Cloud IoT Core accepte les algorithmes de signature suivants:

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

Outre 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 JWT contient un ensemble de revendications et 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 la durée de vie du jeton. Comme l'en-tête JWT, l'ensemble de revendications JWT est un objet JSON qui est utilisé dans le calcul de la signature.

Revendications requises

Cloud IoT Core requiert 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 "Issued at"): horodatage de la création du jeton, spécifié en secondes depuis le 1er janvier 1970 à 00:00:00 UTC. Le serveur peut signaler une erreur si cet horodatage est trop éloigné dans le passé ou le futur (avec un décalage de 10 minutes).
exp ("Expiration"): horodatage du délai de validité du jeton spécifié, 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 + décalage.
  • Toutes les connexions MQTT seront fermées par le serveur quelques secondes après l'expiration du jeton (ce qui permet d'obtenir un décalage), car MQTT ne permet pas d'actualiser les identifiants. Un nouveau jeton doit être utilisé pour la reconnexion. Notez qu'en raison de l'asymétrie autorisée, la durée de vie minimale d'un jeton est en pratique égale à l'asymétrie acceptable, même si elle est définie sur une seconde.
  • Lors de la connexion via 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 sur lequel l'appareil est enregistré. Si la requête de connexion ne correspond pas à cet ID de projet, l'authentification sera refusée sans autre analyse.

La revendication nbf("NotBefore") 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 JWT

La spécification JSON Web Signature (JWS) guide la mécanique de génération de la signature pour le jeton JWT. L'entrée de la signature est le 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 (telle qu'un fichier rsa_private.pem) à 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 présente un jeton 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 décrit dans la section Revendications requises, les jetons ont des dates d'expiration. Si un appareil est connecté via MQTT et que son jeton expire, l'appareil 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 = ((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,
    )