Authentifizierung zwischen Diensten

Neben der Authentifizierung von Nutzern müssen Sie möglicherweise anderen Diensten gestatten, mit Ihrer API zu interagieren. Für die sichere Kommunikation zwischen Diensten benötigen Sie einen anderen Ansatz als für Clientanwendungen, die Nutzern eine webbasierte Anmeldeaufforderung zum Senden ihrer Anmeldedaten bereitstellen können. Auf dieser Seite wird die Methode gezeigt, die wir zum Implementieren der zwischendienstlichen Authentifizierung empfehlen, begleitet von Beispielcode.

Übersicht

Zur Identifizierung eines Dienstes, der Anfragen an die API sendet, wird ein Dienstkonto verwendet. Mit dem privaten Schlüssel des Dienstkontos signiert der aufrufende Dienst ein sicheres JSON Web Token (JWT) und sendet dieses in der Anfrage an die API.

So implementieren Sie die Authentifizierung zwischen Diensten in der API und im aufrufenden Dienst:

  1. Erstellen Sie ein Dienstkonto und einen Schlüssel für den aufrufenden Dienst.
  2. Konfigurieren Sie die Authentifizierung im OpenAPI-Dokument für Ihren Cloud Endpoints-Dienst.
  3. Ergänzen Sie den Code des aufrufenden Dienstes so, dass:

    • ein JWT erstellt und mit dem privaten Schlüssel des Dienstkontos signiert wird und
    • das signierte JWT in einer Anfrage an die API gesendet wird.

Der ESP überprüft, ob die Anforderungen im JWT mit der Konfiguration im OpenAPI-Dokument übereinstimmen, bevor er die Anfrage an die API weiterleitet. Welche Cloud Identity-Berechtigungen dem Dienstkonto erteilt wurden, überprüft der ESP nicht.

Vorbereitung

Folgende Voraussetzungen sollten erfüllt sein:

Dienstkonto mit einem Schlüssel erstellen

Sie benötigen ein Dienstkonto mit einer privaten Schlüsseldatei, die der aufrufende Dienst zum Signieren des JWT verwendet. Wenn mehrere Dienste Anfragen an Ihre API senden, können Sie ein einziges Dienstkonto für alle aufrufenden Dienste erstellen. Falls Sie zwischen den Diensten unterscheiden müssen, weil diese beispielsweise unterschiedliche Berechtigungen haben, können Sie pro aufrufendem Dienst ein Dienstkonto und einen Schlüssel erstellen.

In diesem Abschnitt wird erläutert, wie Sie die Google Cloud Console und das gcloud-Befehlszeilentool erstellen, um das Dienstkonto und die Datei mit dem privaten Schlüssel zu erstellen und dem Dienstkonto die Rolle Ersteller von Dienstkonto-Tokens zuzuweisen. Wie Sie diese Schritte mithilfe einer API ausführen, erfahren Sie unter Dienstkonten erstellen und verwalten.

So erstellen Sie ein Dienstkonto und eine Schlüsseldatei:

Konsole

  1. Erstellen Sie ein Dienstkonto:

    1. Rufen Sie in der Console die Seite Dienstkonto erstellen auf.

      Zur Seite "Dienstkonto erstellen"

    2. Wählen Sie das Projekt aus, das Sie verwenden möchten.

    3. Geben Sie im Feld Dienstkontoname einen Namen ein.

    4. Optional: Geben Sie im Feld Dienstkontobeschreibung eine Beschreibung ein.

    5. Klicken Sie auf Erstellen.

    6. Klicken Sie auf das Feld Rolle auswählen. Wählen Sie unter Alle Rollen Dienstkonto > Ersteller von Dienstkonto-Tokens aus.

    7. Klicken Sie auf Fertig.

      Schließen Sie das Browserfenster nicht. Sie verwenden es in der nächsten Aufgabe.

  2. Erstellen Sie einen Dienstkontoschlüssel:

    1. Klicken Sie in der Konsole auf die E-Mail-Adresse des erstellten Dienstkontos.
    2. Klicken Sie auf Schlüssel.
    3. Klicken Sie auf Schlüssel hinzufügen > Neuen Schlüssel erstellen.
    4. Klicken Sie auf Erstellen. Eine JSON-Datei mit dem privaten Schlüssel des Dienstkontos wird auf Ihren Computer heruntergeladen.
    5. Klicken Sie auf Schließen.

gcloud

Sie können die folgenden Befehle mit der Google Cloud-Befehlszeile auf Ihrem lokalen Computer oder in Cloud Shell ausführen.

  1. Legen Sie das Standardkonto für gcloud fest. Wenn Sie mehrere Konten haben, müssen Sie das Konto auswählen, das sich im zu verwendenden Google Cloud-Projekt befindet:

    gcloud auth login
    
  2. Rufen Sie die Projekt-IDs für Ihre Google Cloud-Projekte ab.

    gcloud projects list
    
  3. Legen Sie das Standardprojekt fest. Ersetzen Sie PROJECT_ID durch die Google Cloud-Projekt-ID, die Sie verwenden möchten.

    gcloud config set project PROJECT_ID
  4. Dienstkonto erstellen Ersetzen Sie SA_NAME und SA_DISPLAY_NAME durch den zu verwendenden Namen bzw. Anzeigenamen:

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
    
  5. Rufen Sie die E-Mail-Adresse des gerade erstellten Dienstkontos ab:

    gcloud iam service-accounts list
    
  6. Fügen Sie die Rolle Ersteller von Dienstkonto-Tokens hinzu. Ersetzen Sie SA_EMAIL_ADDRESS durch die E-Mail-Adresse des Dienstkontos.

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/iam.serviceAccountTokenCreator
    
  7. Erstellen Sie im aktuellen Arbeitsverzeichnis eine Schlüsseldatei für das Dienstkonto. Ersetzen Sie dabei FILE_NAME durch den Namen, den Sie für die Schlüsseldatei verwenden möchten. Mit dem Befehl gcloud wird standardmäßig eine JSON-Datei erstellt.

    gcloud iam service-accounts keys create FILE_NAME.json \
      --iam-account SA_EMAIL_ADDRESS
    

Weitere Informationen zu den genannten Befehlen finden Sie in der Referenz zu gcloud.

Informationen zum Schutz des privaten Schlüssels finden Sie unter Best Practices für die Verwaltung von Anmeldedaten.

API für die Authentifizierung konfigurieren

Das OpenAPI-Dokument muss ein Objekt für Sicherheitsanforderungen und ein Objekt für Sicherheitsdefinitionen enthalten, damit ESP die Anforderungen im signierten JWT validieren kann.

  1. Fügen Sie das Dienstkonto als Aussteller in das OpenAPI-Dokument ein:

    securityDefinitions:
      DEFINITION_NAME:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "SA_EMAIL_ADDRESS"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS"
    
    • Ersetzen Sie DEFINITION_NAME durch einen String, der diese Sicherheitsdefinition bezeichnet. Sie könnten dazu den Namen des Dienstkontos verwenden oder einen Namen, der den anrufenden Dienst identifiziert.
    • Ersetzen Sie SA_EMAIL_ADDRESS durch die E-Mail-Adresse des Dienstkontos.
    • Sie können mehrere Sicherheitsdefinitionen im OpenAPI-Dokument angeben, allerdings muss jede Definition einen anderen x-google-issuer haben. Nachdem Sie getrennte Dienstkonten für jeden aufrufenden Dienst erstellt haben, legen Sie für jeden Dienst eine Sicherheitsdefinition fest. Beispiel:
    securityDefinitions:
      service-1:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com"
      service-2:
        authorizationUrl: ""
        flow: "implicit"
        type: "oauth2"
        x-google-issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
        x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
    
  2. Fügen Sie optional x-google-audiences in den Abschnitt securityDefinitions ein. Wenn Sie x-google-audiences nicht hinzufügen, erfordert der ESP, dass der Anspruch "aud" (Zielgruppe) im JWT das Format https://SERVICE_NAME hat, wobei SERVICE_NAME der Name Ihres Endpoints-Dienstes ist, den Sie im Feld host Ihres OpenAPI-Dokuments konfiguriert haben.

  3. Fügen Sie optional x-google-jwt-locations in den Abschnitt securityDefinitions ein. Sie können diesen Wert verwenden, um einen benutzerdefinierten JWT-Speicherort zu definieren. Die Standard-JWT-Standorte sind der Header Authorization (mit dem Präfix "Bearer"), der Header X-Goog-Iap-Jwt-Assertion oder der Abfrageparameter access_token. Hinweis:

    • Wenn Sie x-google-jwt-locations angeben, ignoriert Endpoints alle Standardspeicherorte.
    • x-google-jwt-locations wird nur von ESPv2 unterstützt.
  4. Fügen Sie den Abschnitt security entweder auf der obersten Ebene der Datei (nicht eingerückt oder verschachtelt) ein, um ihn auf die gesamte API anzuwenden, oder auf der Methodenebene, um ihn auf eine bestimmte Methode zu beschränken. Wenn Sie den Abschnitt security sowohl auf der API-Ebene als auch auf der Methodenebene verwenden, werden die Einstellungen auf der API-Ebene durch die Einstellungen auf der Methodenebene überschrieben.

    security:
      - DEFINITION_NAME: []
    
    • Ersetzen Sie DEFINITION_NAME durch den Namen, den Sie im Abschnitt securityDefinitions verwendet haben.
    • Wenn Sie mehrere Definitionen im Abschnitt securityDefinitions haben, fügen Sie sie im Abschnitt security hinzu. Beispiel:

      security:
        - service-1: []
        - service-2: []
      
  5. Stellen Sie das aktualisierte OpenAPI-Dokument bereit. Ersetzen Sie dabei OPENAPI_DOC durch den Namen des OpenAPI-Dokuments.

    gcloud endpoints services deploy OPENAPI_DOC

Bevor der ESP eine Anfrage an die API weiterleitet, überprüft er:

  • Die Signatur des JWT mithilfe des öffentlichen Schlüssels, der sich im URI befindet und im Feld x-google-jwks_uri des OpenAPI-Dokuments angegeben wurde.
  • Ob die Anforderung "iss" (issuer) im JWT mit dem im Feld x-google-issuer angegebenen Wert übereinstimmt.
  • Ob die Anforderung "aud" (audience) im JWT den Namen des Endpoints-Dienstes enthält oder mit einem der Werte übereinstimmt, die Sie im Feld x-google-audiences angegeben haben.
  • Ob das Token abgelaufen ist. Das erfolgt mithilfe der Anforderung "exp" (expiration time).

Weitere Informationen zu x-google-issuer, x-google-jwks_uri, x-google-audiences und x-google-jwt-locations finden Sie unter OpenAPI-Erweiterungen.

Authentifizierte Anfrage an eine Endpoints API senden

Bei einer authentifizierten Anfrage sendet der aufrufende Dienst ein JWT, das vom Dienstkonto signiert wurde, das Sie im OpenAPI-Dokument angegeben haben. Der anrufende Dienst muss folgende Schritte ausführen:

  1. Ein JWT erstellen und mit dem privaten Schlüssel des Dienstkontos signieren.
  2. Das signierte JWT in einer Anfrage an die API senden.

Der folgende Beispielcode veranschaulicht diesen Prozess für ausgewählte Sprachen. Um eine authentifizierte Anfrage in anderen Sprachen zu stellen, finden Sie unter jwt.io eine Liste der unterstützten Bibliotheken.

  1. Fügen Sie im aufrufenden Dienst die folgende Funktion hinzu und übergeben Sie die folgenden Parameter:
    Java
    • saKeyfile: Der vollständige Pfad zum privaten Schlüssel des Dienstkontos.
    • saEmail: Die E-Mail-Adresse des Dienstkontos.
    • audience: Wenn Sie das Feld x-google-audiences in das OpenAPI-Dokument eingefügt haben, legen Sie audience auf einen der Werte fest, die Sie für x-google-audiences angegeben haben. Setzen Sie andernfalls audience auf https://SERVICE_NAME, wobei SERVICE_NAME der Name des Endpoints-Dienstes ist.
    • expiryLength: Die Ablaufzeit des JWT in Sekunden.
    Python
    • sa_keyfile: Der vollständige Pfad zum privaten Schlüssel des Dienstkontos.
    • sa_email: Die E-Mail-Adresse des Dienstkontos.
    • audience: Wenn Sie das Feld x-google-audiences in das OpenAPI-Dokument eingefügt haben, legen Sie audience auf einen der Werte fest, die Sie für x-google-audiences angegeben haben. Setzen Sie andernfalls audience auf https://SERVICE_NAME, wobei SERVICE_NAME der Name des Endpoints-Dienstes ist.
    • expiry_length: Die Ablaufzeit des JWT in Sekunden.
    Go
    • saKeyfile: Der vollständige Pfad zum privaten Schlüssel des Dienstkontos.
    • saEmail: Die E-Mail-Adresse des Dienstkontos.
    • audience: Wenn Sie das Feld x-google-audiences in das OpenAPI-Dokument eingefügt haben, legen Sie audience auf einen der Werte fest, die Sie für x-google-audiences angegeben haben. Setzen Sie andernfalls audience auf https://SERVICE_NAME, wobei SERVICE_NAME der Name des Endpoints-Dienstes ist.
    • expiryLength: Die Ablaufzeit des JWT in Sekunden.

    Die Funktion erstellt ein JWT, signiert es mithilfe der Datei mit dem privaten Schlüssel und gibt das signierte JWT zurück.

    Java
    /**
     * Generates a signed JSON Web Token using a Google API Service Account
     * utilizes com.auth0.jwt.
     */
    public static String generateJwt(final String saKeyfile, final String saEmail,
        final String audience, final int expiryLength)
        throws FileNotFoundException, IOException {
    
      Date now = new Date();
      Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
    
      // Build the JWT payload
      JWTCreator.Builder token = JWT.create()
          .withIssuedAt(now)
          // Expires after 'expiryLength' seconds
          .withExpiresAt(expTime)
          // Must match 'issuer' in the security configuration in your
          // swagger spec (e.g. service account email)
          .withIssuer(saEmail)
          // Must be either your Endpoints service name, or match the value
          // specified as the 'x-google-audience' in the OpenAPI document
          .withAudience(audience)
          // Subject and email should match the service account's email
          .withSubject(saEmail)
          .withClaim("email", saEmail);
    
      // Sign the JWT with a service account
      FileInputStream stream = new FileInputStream(saKeyfile);
      ServiceAccountCredentials cred = ServiceAccountCredentials.fromStream(stream);
      RSAPrivateKey key = (RSAPrivateKey) cred.getPrivateKey();
      Algorithm algorithm = Algorithm.RSA256(null, key);
      return token.sign(algorithm);
    }
    Python
    def generate_jwt(sa_keyfile,
                     sa_email='account@project-id.iam.gserviceaccount.com',
                     audience='your-service-name',
                     expiry_length=3600):
    
        """Generates a signed JSON Web Token using a Google API Service Account."""
    
        now = int(time.time())
    
        # build payload
        payload = {
            'iat': now,
            # expires after 'expiry_length' seconds.
            "exp": now + expiry_length,
            # iss must match 'issuer' in the security configuration in your
            # swagger spec (e.g. service account email). It can be any string.
            'iss': sa_email,
            # aud must be either your Endpoints service name, or match the value
            # specified as the 'x-google-audience' in the OpenAPI document.
            'aud':  audience,
            # sub and email should match the service account's email address
            'sub': sa_email,
            'email': sa_email
        }
    
        # sign with keyfile
        signer = google.auth.crypt.RSASigner.from_service_account_file(sa_keyfile)
        jwt = google.auth.jwt.encode(signer, payload)
    
        return jwt
    Go
    
    // generateJWT creates a signed JSON Web Token using a Google API Service Account.
    func generateJWT(saKeyfile, saEmail, audience string, expiryLength int64) (string, error) {
    	now := time.Now().Unix()
    
    	// Build the JWT payload.
    	jwt := &jws.ClaimSet{
    		Iat: now,
    		// expires after 'expiryLength' seconds.
    		Exp: now + expiryLength,
    		// Iss must match 'issuer' in the security configuration in your
    		// swagger spec (e.g. service account email). It can be any string.
    		Iss: saEmail,
    		// Aud must be either your Endpoints service name, or match the value
    		// specified as the 'x-google-audience' in the OpenAPI document.
    		Aud: audience,
    		// Sub and Email should match the service account's email address.
    		Sub:           saEmail,
    		PrivateClaims: map[string]interface{}{"email": saEmail},
    	}
    	jwsHeader := &jws.Header{
    		Algorithm: "RS256",
    		Typ:       "JWT",
    	}
    
    	// Extract the RSA private key from the service account keyfile.
    	sa, err := ioutil.ReadFile(saKeyfile)
    	if err != nil {
    		return "", fmt.Errorf("Could not read service account file: %v", err)
    	}
    	conf, err := google.JWTConfigFromJSON(sa)
    	if err != nil {
    		return "", fmt.Errorf("Could not parse service account JSON: %v", err)
    	}
    	block, _ := pem.Decode(conf.PrivateKey)
    	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    	if err != nil {
    		return "", fmt.Errorf("private key parse error: %v", err)
    	}
    	rsaKey, ok := parsedKey.(*rsa.PrivateKey)
    	// Sign the JWT with the service account's private key.
    	if !ok {
    		return "", errors.New("private key failed rsa.PrivateKey type assertion")
    	}
    	return jws.Encode(jwsHeader, jwt, rsaKey)
    }
    
  2. Fügen Sie im aufrufenden Dienst die folgende Funktion hinzu, um das signierte JWT in der Anfrage Authorization: Bearer an die API zu senden:
    Java
    /**
     * Makes an authorized request to the endpoint.
     */
    public static String makeJwtRequest(final String signedJwt, final URL url)
        throws IOException, ProtocolException {
    
      HttpURLConnection con = (HttpURLConnection) url.openConnection();
      con.setRequestMethod("GET");
      con.setRequestProperty("Content-Type", "application/json");
      con.setRequestProperty("Authorization", "Bearer " + signedJwt);
    
      InputStreamReader reader = new InputStreamReader(con.getInputStream());
      BufferedReader buffReader = new BufferedReader(reader);
    
      String line;
      StringBuilder result = new StringBuilder();
      while ((line = buffReader.readLine()) != null) {
        result.append(line);
      }
      buffReader.close();
      return result.toString();
    }
    Python
    def make_jwt_request(signed_jwt, url='https://your-endpoint.com'):
        """Makes an authorized request to the endpoint"""
        headers = {
            'Authorization': 'Bearer {}'.format(signed_jwt.decode('utf-8')),
            'content-type': 'application/json'
        }
        response = requests.get(url, headers=headers)
        print(response.status_code, response.content)
        response.raise_for_status()
    
    Go
    
    // makeJWTRequest sends an authorized request to your deployed endpoint.
    func makeJWTRequest(signedJWT, url string) (string, error) {
    	client := &http.Client{
    		Timeout: 10 * time.Second,
    	}
    
    	req, err := http.NewRequest("GET", url, nil)
    	if err != nil {
    		return "", fmt.Errorf("failed to create HTTP request: %v", err)
    	}
    	req.Header.Add("Authorization", "Bearer "+signedJWT)
    	req.Header.Add("content-type", "application/json")
    
    	response, err := client.Do(req)
    	if err != nil {
    		return "", fmt.Errorf("HTTP request failed: %v", err)
    	}
    	defer response.Body.Close()
    	responseData, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %v", err)
    	}
    	return string(responseData), nil
    }
    

Wenn Sie eine Anfrage mit einem JWT senden, empfehlen wir aus Sicherheitsgründen, das Authentifizierungstoken in den Header Authorization: Bearer einzufügen. Beispiel:

curl --request POST \
  --header "Authorization: Bearer ${TOKEN}" \
  "${ENDPOINTS_HOST}/echo"

Dabei sind ENDPOINTS_HOST und TOKEN Umgebungsvariablen, die den Hostnamen der API bzw. das Authentifizierungstoken enthalten.

Authentifizierte Ergebnisse in Ihrer API empfangen

Der ESP leitet in der Regel alle empfangenen Header weiter. Der ESP überschreibt aber möglicherweise den ursprünglichen Authorization-Header, wenn die Back-End-Adresse in der OpenAPI-Spezifikation durch x-google-backend oder in der gRPC-Dienstkonfiguration mit BackendRule angegeben wird.

Der ESP sendet das Authentifizierungsergebnis in X-Endpoint-API-UserInfo an die Back-End-API. Wir empfehlen die Verwendung dieses Header anstelle des ursprünglichen Authorization-Headers. Dieser Header ist ein String, der base64url ein JSON-Objekt codiert. Das JSON-Objektformat unterscheidet sich zwischen ESPv2 und ESP. Bei ESPv2 ist das JSON-Objekt genau die ursprüngliche JWT-Nutzlast. Für den ESP verwendet das JSON-Objekt verschiedene Feldnamen und platziert die ursprüngliche JWT-Nutzlast unter dem Feld claims. Weitere Informationen zum Format finden Sie unter JWTs im Back-End-Dienst verarbeiten.

Weitere Informationen