JSON-Web-Tokens (JWTs) verwenden

Zur Authentifizierung bei Cloud IoT Core muss jedes Gerät ein JSON Web Token (JWT, RFC 7519) vorbereiten. JWTs werden für die kurzlebige Authentifizierung zwischen Geräten und den MQTT- oder HTTP-Brücken verwendet. Auf dieser Seite werden die Cloud IoT Core-Anforderungen für den Inhalt des JWT beschrieben.

Für Cloud IoT Core ist keine bestimmte Methode zur Tokengenerierung erforderlich. Eine gute Sammlung von Hilfsclient-Bibliotheken finden Sie auf JWT.io.

Beim Erstellen eines MQTT-Clients muss das JWT im Feld password der Nachricht CONNECT übergeben werden. Beim Herstellen einer Verbindung über HTTP muss ein JWT im Header jeder HTTP-Anfrage enthalten sein.

JWTs erstellen

JWTs bestehen aus drei Abschnitten: einem Header, einer Nutzlast (mit einem Anforderungssatz) und einer Signatur. Der Header und die Nutzlast sind JSON-Objekte, die in UTF-8-Byte serialisiert und dann mit base64url-Codierung codiert werden.

Der Header, die Nutzlast und die Signatur des JWT werden mit Punkten (.) verkettet. Daher hat ein JWT in der Regel die folgende Form:

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

Im folgenden Beispiel wird gezeigt, wie Sie ein Cloud IoT Core-JWT für ein bestimmtes Projekt erstellen. Nachdem Sie das JWT erstellt haben, können Sie eine Verbindung zur MQTT- oder HTTP-Bridge herstellen, um Nachrichten von einem Gerät zu veröffentlichen.

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)

JWT-Header

Der JWT-Header besteht aus zwei Feldern, die den Signaturalgorithmus und den Tokentyp angeben. Beide Felder sind obligatorisch und jedes Feld hat nur einen Wert. Cloud IoT Core unterstützt die folgenden Signaturalgorithmen:

  • JWT RS256 (RSASSA-PKCS1-v1_5 mit SHA-256 RFC 7518 sec 3.3). Dies wird als RS256 im Feld alg im JWT-Header ausgedrückt.
  • JWT ES256 (ECDSA mit P-256 und SHA-256 RFC 7518 sec 3.4) ist in OpenSSL als die Kurve prime256v1 definiert. Dies wird als ES256 im Feld alg im JWT-Header ausgedrückt.

Zusätzlich zum Signaturalgorithmus müssen Sie das JWT-Tokenformat angeben.

Die JSON-Darstellung des Headers sieht so aus:

Für RSA-Schlüssel:

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

Für Elliptische-Kurven-Schlüssel:

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

Der im Header angegebene Algorithmus muss mit mindestens einem der für das Gerät registrierten öffentlichen Schlüssel übereinstimmen.

JWT-Anforderungen

Die JWT-Nutzlast enthält eine Reihe von Anforderungen und wird mit den asymmetrischen Schlüsseln signiert. Der JWT-Anforderungssatz enthält Informationen zum JWT, z. B. das Ziel des Tokens, den Aussteller, den Zeitpunkt der Ausstellung des Tokens und/oder die Lebensdauer des Tokens. Wie der JWT-Header ist auch der JWT-Anforderungssatz ein JSON-Objekt, das zur Berechnung der Signatur verwendet wird.

Erforderliche Ansprüche

Cloud IoT Core erfordert die folgenden reservierten Anforderungsfelder. Sie können in beliebiger Reihenfolge im Anforderungssatz angezeigt werden.

Name Beschreibung
iat ("Ausgegeben am"): Der Zeitstempel, zu dem das Token erstellt wurde, angegeben in Sekunden seit 00:00:00 UTC, 1. Januar 1970. Der Server meldet möglicherweise einen Fehler, wenn dieser Zeitstempel zu weit in der Vergangenheit oder in der Zukunft liegt (erlaubt eine Abweichung von 10 Minuten).
exp ("Ablauf"): Der Zeitstempel, ab wann das Token nicht mehr gültig ist, angegeben als Sekunden seit 00:00:00 UTC, 1. Januar 1970. Die maximale Lebensdauer eines Tokens beträgt 24 Stunden + Abweichung.
  • Alle MQTT-Verbindungen werden vom Server einige Sekunden nach Ablauf des Tokens geschlossen (unter Berücksichtigung der Abweichung), da MQTT keine Möglichkeit zur Aktualisierung der Anmeldeinformationen bietet. Ein neues Token muss erstellt werden, um die Verbindung wiederherzustellen. Aufgrund der zulässigen Abweichung entspricht die Mindestlebensdauer eines Tokens in der Praxis der akzeptablen Taktabweichung, auch wenn sie auf eine Sekunde eingestellt ist.
  • Wenn Sie eine Verbindung über HTTP herstellen, muss jede HTTP-Anfrage unabhängig von der Ablaufzeit ein JWT enthalten.
  • Beachten Sie, dass Clients in NTP-fähigen Geräten (Network Time Protocol) den Google Public NTP Server verwenden können, um die Uhr des Geräts synchron zu halten; die Anforderung für die Authentifizierung besteht darin, die Uhr mit einer Abweichung von bis zu 10 Minuten zu synchronisieren.
aud ("Zielgruppe"): Dies muss ein einzelner String sein, der die Cloud-Projekt-ID enthält, unter der das Gerät registriert ist. Wenn die Verbindungsanfrage nicht mit dieser Projekt-ID übereinstimmt, wird die Authentifizierung ohne weitere Analyse abgelehnt.

Die Anforderung nbf("Not before") wird ignoriert und ist nicht erforderlich.

Im Folgenden finden Sie eine JSON-Darstellung der erforderlichen reservierten Felder in einem Anforderungssatz des Cloud IoT Core-JWT:

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

JWT-Signatur

Die Spezifikation JSON-Websignatur (JWS) regelt, wie die Signatur für das JWT erzeugt wird. Die Eingabe für die Signatur ist das Byte-Array des folgenden Inhalts:

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

Zur Berechnung der Signatur signieren Sie den base64url-codierten Header, den base64-url-codierten Anforderungssatz und einen geheimen Schlüssel (z. B. eine rsa_private.pem-Datei) mit dem im Header definierten Algorithmus. Die Signatur ist dann base64url-codiert und das Ergebnis ist das JWT. Das folgende Beispiel zeigt ein JWT vor der base64url-Codierung:

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

Nach der endgültigen Codierung sieht das JWT so aus:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJteS1wcm9qZWN0IiwiZXhwIjoxNTA5NjUwODAxLCJpYXQiOjE1MDk2NTQ0MDF9.F4iKO0R0wvHkpCcQoyrYttdGxE5FLAgDhbTJQLEHIBPsbL2WkLxXB9IGbDESn9rE7oxn89PJFRtcLn7kJwvdQkQcsPxn2RQorvDAnvAi1w3k8gpxYWo2DYJlnsi7mxXDqSUCNm1UCLRCW68ssYJxYLSg7B1xGMgDADGyYPaIx1EdN4dDbh-WeDyLLa7a8iWVBXdbmy1H3fEuiAyxiZpk2ll7DcQ6ryyMrU2XadwEr9PDqbLe5SrlaJsQbFi8RIdlQJSo_DZGOoAlA5bYTDYXb-skm7qvoaH5uMtOUb0rjijYuuxhNZvZDaBerEaxgmmlO0nQgtn12KVKjmKlisG79Q

JWTs aktualisieren

Wie unter Erforderliche Anforderungen beschrieben, haben Tokens ein Ablaufdatum. Wenn ein Gerät über MQTT verbunden ist und dessen Token abläuft, wird die Verbindung zu Cloud IoT Core automatisch getrennt. Sie können verhindern, dass das Gerät die Verbindung unterbricht. Aktualisieren Sie dazu das Token automatisch. Die folgenden Beispiele veranschaulichen, wie geprüft wird, ob ein Token abgelaufen ist, und wie, falls dies der Fall ist, die Verbindung mit einem neuen Token wiederhergestellt wird, ohne das Gerät zu trennen.

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