Google Cloud IoT Core verrà ritirato il 16 agosto 2023. Per saperne di più, contatta il team dedicato al tuo account Google Cloud.

Utilizzo di token web JSON (JWT)

Per eseguire l'autenticazione in Cloud IoT Core, ogni dispositivo deve preparare un token web JSON (JWT, RFC 7519). I JWT vengono utilizzati per l'autenticazione di breve durata tra i dispositivi e i bridge MQTT o HTTP. In questa pagina vengono descritti i requisiti di Cloud IoT Core per i contenuti del JWT.

Cloud IoT Core non richiede un metodo di generazione di token specifico. Una buona raccolta di librerie client helper è disponibile su JWT.io.

Quando crei un client MQTT, il JWT deve essere trasmesso nel campo password del messaggio CONNECT. Quando ti connetti tramite HTTP, devi includere un JWT nell'intestazione di ogni richiesta HTTP.

Creazione di JWT

I JWT sono composti da tre sezioni: un'intestazione, un payload (contenente un set di rivendicazioni) e una firma. L'intestazione e il payload sono oggetti JSON, che vengono serializzati in byte UTF-8 e poi codificati utilizzando la codifica base64url.

L'intestazione, il payload e la firma del JWT sono concatenati con punti (.). Di conseguenza, un JWT in genere ha il seguente formato:

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

L'esempio seguente illustra come creare un JWT Cloud IoT Core per un determinato progetto. Dopo aver creato il JWT, puoi connetterti al bridge MQTT o HTTP per pubblicare i messaggi da un 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 {
  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)

Intestazione JWT

L'intestazione JWT è costituita da due campi che indicano l'algoritmo di firma e il tipo di token. Entrambi i campi sono obbligatori e ogni campo ha un solo valore. Cloud IoT Core supporta i seguenti algoritmi di firma:

  • JWT RS256 (RSASSA-PKCS1-v1_5 utilizzando SHA-256 RFC 7518 sec 3.3). Questo valore è espresso come RS256 nel campo alg dell'intestazione JWT.
  • JWT ES256 (ECDSA utilizzando P-256 e SHA-256 RFC 7518 sec 3.4), definito in OpenSSL come curva prime256v1. Questo valore è espresso come ES256 nel campo alg dell'intestazione JWT.

Oltre all'algoritmo di firma, devi fornire il formato del token JWT.

La rappresentazione JSON dell'intestazione è la seguente:

Per le chiavi RSA:

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

Per le chiavi Curve ellittiche:

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

L'algoritmo specificato nell'intestazione deve corrispondere ad almeno una delle chiavi pubbliche registrate per il dispositivo.

Rivendicazioni di JWT

Il payload JWT contiene un insieme di rivendicazioni e viene firmato utilizzando le chiavi asimmetriche. Il set di rivendicazioni JWT contiene informazioni sul JWT, come la destinazione del token, l'emittente, l'ora in cui è stato emesso e/o la durata del token. Come l'intestazione JWT, il set di rivendicazioni JWT è un oggetto JSON e viene utilizzato nel calcolo della firma.

Rivendicazioni obbligatorie

Cloud IoT Core richiede i seguenti campi di rivendicazione riservati. Potranno essere mostrate in qualsiasi ordine nel set di rivendicazioni.

Nome Descrizione
iat ("Emesso alle"): il timestamp di creazione del token, specificato in secondi dalle 00:00:00 UTC, 1 gennaio 1970. Il server potrebbe segnalare un errore se questo timestamp è troppo lontano nel passato o nel futuro (consentendo 10 minuti di disallineamento).
exp ("Scadenza"): il timestamp di validità del token, specificato in secondi dalle 00:00:00 UTC, 1 gennaio 1970. La durata massima di un token è 24 ore + disallineamento.
  • Tutte le connessioni MQTT verranno chiuse dal server alcuni secondi dopo la scadenza del token (per consentire il disallineamento), in quanto MQTT non ha modo di aggiornare le credenziali. Per poter riconnettere il nuovo token, deve essere creato un nuovo token. Tieni presente che, a causa del disallineamento consentito, in pratica la durata minima di un token sarà uguale al disallineamento accettabile dell'orologio, anche se è impostato su un secondo.
  • Quando esegui la connessione tramite HTTP, ogni richiesta HTTP deve includere un JWT, indipendentemente dalla scadenza.
  • Tieni presente che i client con dispositivi che supportano il protocollo Time Network Protocol (NTP) possono utilizzare il server pubblico NTP di Google per mantenere sincronizzato l'orologio del dispositivo; il requisito per l'autenticazione è mantenere l'orologio sincronizzato con un disallineamento fino a 10 minuti.
aud ("Pubblico"): deve essere una singola stringa contenente l'ID progetto cloud in cui è registrato il dispositivo. Se la richiesta di connessione non corrisponde a questo ID progetto, l'autenticazione verrà rifiutata senza ulteriori analisi.

La rivendicazione nbf("Not Before") verrà ignorata e non è obbligatoria.

Di seguito è riportata una rappresentazione JSON dei campi riservati obbligatori in un set di richieste JWT di Cloud IoT Core:

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

Firma JWT

La specifica JSON Web Signature (JWS) definisce i meccanismi della generazione della firma per JWT. L'input della firma è l'array di byte dei seguenti contenuti:

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

Per calcolare la firma, firma l'intestazione con codifica base64url, il set di rivendicazioni con codifica base64-url e una chiave segreta (ad esempio un file rsa_private.pem) utilizzando l'algoritmo che hai definito nell'intestazione. La firma viene quindi codificata in base64url e il risultato è JWT. Nell'esempio seguente viene mostrato un JWT prima della codifica base64url:

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

Dopo la codifica finale, il JWT ha il seguente aspetto:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJteS1wcm9qZWN0IiwiZXhwIjoxNTA5NjUwODAxLCJpYXQiOjE1MDk2NTQ0MDF9.F4iKO0R0wvHkpCcQoyrYttdGxE5FLAgDhbTJQLEHIBPsbL2WkLxXB9IGbDESn9rE7oxn89PJFRtcLn7kJwvdQkQcsPxn2RQorvDAnvAi1w3k8gpxYWo2DYJlnsi7mxXDqSUCNm1UCLRCW68ssYJxYLSg7B1xGMgDADGyYPaIx1EdN4dDbh-WeDyLLa7a8iWVBXdbmy1H3fEuiAyxiZpk2ll7DcQ6ryyMrU2XadwEr9PDqbLe5SrlaJsQbFi8RIdlQJSo_DZGOoAlA5bYTDYXb-skm7qvoaH5uMtOUb0rjijYuuxhNZvZDaBerEaxgmmlO0nQgtn12KVKjmKlisG79Q

Aggiornamento dei JWT in corso...

Come descritto nelle rivendicazioni richieste, i token hanno date di scadenza. Se un dispositivo si connette tramite MQTT e il relativo token scade, il dispositivo si disconnette automaticamente da Cloud IoT Core. Puoi impedire al dispositivo di disconnettersi aggiornando automaticamente il token. I seguenti esempi illustrano come verificare se un token è scaduto e, in caso affermativo, come riconnettersi con un nuovo token senza disconnettere il dispositivo.

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,
    )