Utilizzo di token web JSON (JWT)

Per l'autenticazione su 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 di 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 passato nel campo password del messaggio CONNECT. Quando si connette tramite HTTP, un JWT deve essere incluso nell'intestazione di ogni richiesta HTTP.

Creazione di JWT

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

L'intestazione, il payload e la firma di JWT sono concatenati con punti (.). Di conseguenza, un JWT assume in genere 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 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 {
  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)

Intestazione JWT

L'intestazione JWT è composta 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). È indicato come RS256 nel campo alg nell'intestazione JWT.
  • JWT ES256 (ECDSA che utilizza P-256 e SHA-256 RFC 7518 sec 3.4), definito in OpenSSL come curva Pri256v1. È indicato come ES256 nel campo alg nell'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 a curva ellittica:

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

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

Attestazioni JWT

Il payload JWT contiene un insieme di attestazioni e viene firmato utilizzando le chiavi asimmetriche. Il set di attestazioni JWT contiene informazioni su JWT, ad esempio il target del token, l'emittente, la data di emissione del token e/o la durata del token. Come l'intestazione JWT, il set di attestazioni JWT è un oggetto JSON e viene utilizzato nel calcolo della firma.

Rivendicazioni obbligatorie

Cloud IoT Core richiede i seguenti campi di rivendicazione riservati. Queste possono essere visualizzate in qualsiasi ordine nella rivendicazione.

Nome Descrizione
iat ("Issued At"): il timestamp di creazione del token, specificato come secondi dalle ore 00:00:00 UTC, 1 gennaio 1970. Il server potrebbe segnalare un errore se il timestamp è troppo passato o futuro (consentendo 10 minuti per alterare).
exp ("Expiration"): il timestamp della mancata validità del token, specificato come secondi dalle ore 00:00:00 UTC, 1 gennaio 1970. La durata massima di un token è 24 ore + inclinazione.
  • Tutte le connessioni MQTT verranno chiuse dal server alcuni secondi dopo la scadenza del token (consentendo l'assolvimento), perché MQTT non ha un modo per aggiornare le credenziali. Per riconnetterti, deve essere coniato un nuovo token. Tieni presente che, a causa dello sfasamento di autorizzazione consentito, nella pratica la durata minima di un token sarà uguale allo sfasamento di orario accettabile, anche se è impostato su un secondo.
  • Quando ti connetti tramite HTTP, ogni richiesta HTTP deve includere un JWT, indipendentemente dalla data di scadenza.
  • Tieni presente che i client con dispositivi abilitati per Network Time Protocol (NTP) possono utilizzare il Google Public NTP Server per mantenere sincronizzato l'orologio del dispositivo; il requisito per l'autenticazione è mantenere l'orologio sincronizzato con una differenza massima di 10 minuti.
aud ("Audience"): 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à negata senza ulteriori analisi.

Il reclamo nbf("non precedente") verrà ignorato e non è obbligatorio.

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

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

Firma JWT

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

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

Per calcolare la firma, firma l'intestazione con codifica base64, il set di attestazioni 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 codificata in Base64url e il risultato è JWT. L'esempio seguente mostra un JWT prima della codifica dell'URL base64:

{"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

Come descritto nelle rivendicazioni richieste, i token hanno date di scadenza. Se un dispositivo è connesso tramite MQTT e il suo 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 scollegare il 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,
    )