S'authentifier entre différents services

En plus de l'authentification des utilisateurs, vous devrez peut-être autoriser d'autres services à interagir avec l'API. Bien que les applications clientes puissent fournir aux utilisateurs une invite de connexion Web pour soumettre leurs identifiants, vous avez besoin d'une autre approche pour une communication de service à service sécurisée. Cette page présente l'approche recommandée pour la mise en œuvre de l'authentification entre services et fournit un exemple de code.

Présentation

Pour identifier un service qui envoie des requêtes à l'API, vous utilisez un compte de service. Le service appelant utilise la clé privée du compte de service pour signer un jeton Web JSON (JWT) sécurisé et envoie le jeton JWT signé dans la requête à l'API.

Pour mettre en œuvre l'authentification de service à service dans l'API et le service appelant :

  1. Créez un compte de service et une clé pour le service appelant à utiliser.
  2. Ajoutez la compatibilité pour l'authentification dans le document OpenAPI pour le service Cloud Endpoints.
  3. Ajoutez du code au service appelant qui :

    • crée un jeton JWT et le signe avec la clé privée du compte de service ;
    • envoie le jeton JWT signé dans une requête à l'API.

Le proxy ESP vérifie que les revendications du jeton JWT correspondent à la configuration du document OpenAPI avant de transférer la requête à l'API. Il ne vérifie pas les autorisations Cloud Identity accordées sur le compte de service.

Prérequis

Cette page suppose que vous avez déjà :

Créer un compte de service avec une clé

Vous devez disposer d'un compte de service contenant un fichier de clé privée que le service appelant utilise pour signer le jeton JWT. Si plusieurs services envoient des requêtes à l'API, vous pouvez créer un compte de service représentant tous les services appelants. Si vous devez différencier les services (ils peuvent, par exemple, disposer d'autorisations différentes), vous pouvez créer un compte de service et une clé pour chaque service appelant.

Cette section explique comment utiliser la console Google Cloud et l'outil de ligne de commande gcloud pour créer le compte de service et le fichier de clé privée, et pour attribuer au compte de service le rôle Créateur de jetons du compte de service. Pour en savoir plus sur l'utilisation d'une API pour effectuer cette tâche, consultez la section Créer et gérer des comptes de service.

Créez un compte de service et une clé :

Console Google Cloud

  1. Créez un compte de service :

    1. Dans la console Google Cloud, accédez à la page Créer un compte de service.

      Accéder à la page Créer un compte de service

    2. Sélectionnez le projet que vous souhaitez utiliser.

    3. Dans le champ Nom du compte de service, saisissez un nom.

    4. Facultatif : Dans le champ Description du compte de service, saisissez une description.

    5. Cliquez sur Créer.

    6. Cliquez sur le champ Sélectionner un rôle. Sous Tous les rôles, sélectionnez Compte de service > Créateur de jetons du compte de service.

    7. Cliquez sur OK.

      Ne fermez pas la fenêtre de votre navigateur. Vous en aurez besoin lors de la tâche suivante.

  2. Créez une clé de compte de service :

    1. Dans la console Google Cloud, cliquez sur l'adresse e-mail du compte de service que vous avez créé.
    2. Cliquez sur Clés.
    3. Cliquez sur AJOUTER UNE CLÉ -> Créer une clé.
    4. Cliquez sur Créer. Un fichier JSON contenant la clé privée du compte de service est téléchargé sur votre ordinateur.
    5. Cliquez sur Close (Fermer).

gcloud

Vous pouvez exécuter les commandes suivantes à l'aide de la Google Cloud CLI sur votre ordinateur local ou dans Cloud Shell.

  1. Définissez le compte par défaut pour gcloud. Si vous avez plusieurs comptes, veillez à choisir le compte du projet Google Cloud que vous souhaitez utiliser.

    gcloud auth login
    
  2. Saisissez la commande suivante pour afficher les ID de vos projets Google Cloud :

    gcloud projects list
    
  3. Définissez le projet par défaut. Remplacez PROJECT_ID par l'ID du projet Google Cloud que vous souhaitez utiliser.

    gcloud config set project PROJECT_ID
  4. Créez un compte de service. Remplacez SA_NAME et SA_DISPLAY_NAME par le nom et le nom à afficher que vous souhaitez utiliser.

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
    
  5. Affichez l'adresse e-mail du compte de service que vous venez de créer.

    gcloud iam service-accounts list
    
  6. Ajoutez le rôle Créateur de jetons du compte de service. Remplacez SA_EMAIL_ADDRESS par l'adresse e-mail du compte de service.

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/iam.serviceAccountTokenCreator
    
  7. Créez un fichier de clé de compte de service dans le répertoire de travail actuel. Remplacez FILE_NAME par le nom que vous souhaitez utiliser pour le fichier de clé. Par défaut, la commande gcloud crée un fichier JSON.

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

Pour en savoir plus sur les commandes ci-dessus, consultez la documentation de référence sur gcloud.

Pour plus d'informations sur la sauvegarde de la clé privée, consultez la section Bonnes pratiques de gestion des identifiants.

Configurer l'API pour la fonctionnalité d'authentification

Vous devez disposer d'un objet d'exigences de sécurité et d'un objet de définitions de sécurité dans le document OpenAPI pour que le proxy ESP puisse valider les revendications dans le jeton JWT signé.

  1. Ajoutez le compte de service en tant qu'émetteur dans le document OpenAPI.

    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"
    
    • Remplacez DEFINITION_NAME par une chaîne qui identifie cette définition de sécurité. Vous pouvez le remplacer par le nom du compte de service ou un nom qui identifie le service appelant.
    • Remplacez SA_EMAIL_ADDRESS par l'adresse e-mail du compte de service.
    • Vous pouvez établir plusieurs définitions de sécurité dans le document OpenAPI, mais l'émetteur (x-google-issuer) doit être différent pour chaque définition. Si vous avez créé des comptes de service distincts pour chaque service appelant, vous pouvez créer une définition de sécurité pour chaque compte de service, par exemple :
    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. Vous pouvez également ajouter x-google-audiences à la section securityDefinitions. Si vous n'ajoutez pas x-google-audiences, ESP exige que la revendication "aud" (audience) du jeton JWT soit au format https://SERVICE_NAME, où SERVICE_NAME correspond au nom du service Endpoints que vous avez configuré dans le champ host de votre document OpenAPI, sauf si l'option --disable_jwt_audience_service_name_check est utilisée. Si l'option est utilisée et que x-google-audiences n'est pas spécifié, le champ aud du jeton JWT n'est pas coché.

  3. Vous pouvez également ajouter x-google-jwt-locations à la section securityDefinitions. Vous pouvez utiliser cette valeur pour définir un emplacement JWT personnalisé. Les emplacements JWT par défaut sont l'en-tête Authorization (préfixé par "Bearer"), l'en-tête X-Goog-Iap-Jwt-Assertion et le paramètre de requête access_token. Remarque :

    • Si vous spécifiez x-google-jwt-locations, Endpoints ignore tous les emplacements par défaut.
    • x-google-jwt-locations n'est compatible qu'avec ESPv2.
  4. Ajoutez une section security au niveau supérieur du fichier (non imbriqué ni en retrait) pour l'appliquer à l'ensemble de l'API, ou au niveau d'une méthode spécifique pour ne l'appliquer qu'à celle-ci. Attention, si vous utilisez des sections security au niveau de l'API et au niveau de la méthode, les paramètres au niveau de l'API seront ignorés.

    security:
      - DEFINITION_NAME: []
    
    • Remplacez DEFINITION_NAME par le nom que vous avez utilisé dans la section securityDefinitions.
    • Si vous avez plusieurs définitions dans la section securityDefinitions, ajoutez-les dans la section security. Exemple :

      security:
        - service-1: []
        - service-2: []
      
  5. Déployez le document OpenAPI mis à jour. Remplacez OPENAPI_DOC par le nom du document OpenAPI.

    gcloud endpoints services deploy OPENAPI_DOC

Avant que le proxy ESP ne transmette une requête à l'API, il vérifie :

  • la signature du jeton JWT à l'aide de la clé publique, située dans l'URI spécifié dans le champ x-google-jwks_uri du document OpenAPI ;
  • que la revendication "iss" (émetteur) du jeton JWT correspond à la valeur spécifiée dans le champ x-google-issuer ;
  • que la revendication "aud" (audience) du jeton JWT contient le nom du service Endpoints ou correspond à l'une des valeurs que vous avez spécifiées dans le champ x-google-audiences ;
  • que le jeton n'a pas expiré en utilisant la revendication "exp" (date/heure d'expiration).

Pour en savoir plus sur x-google-issuer, x-google-jwks_uri, x-google-audiences et x-google-jwt-locations, consultez la page Extensions OpenAPI.

Effectuer une requête authentifiée à une API Endpoints

Pour effectuer une requête authentifiée, le service appelant envoie un jeton JWT signé par le compte de service que vous avez spécifié dans le document OpenAPI. Le service appelant doit :

  1. Créer un jeton JWT et le signer avec la clé privée du compte de service.
  2. Envoyer le jeton JWT signé dans une requête à l'API.

L'exemple de code suivant illustre ce processus pour certains langages. Pour effectuer une requête authentifiée dans d'autres langages, reportez-vous à jwt.io pour obtenir la liste des bibliothèques compatibles.

  1. Dans le service appelant, ajoutez la fonction suivante et transmettez-lui les paramètres suivants :
    Java
    • saKeyfile : chemin d'accès complet au fichier de clé privée du compte de service.
    • saEmail : adresse e-mail du compte de service.
    • audience : si vous avez ajouté le champ x-google-audiences au document OpenAPI, définissez audience sur l'une des valeurs que vous avez spécifiées pour x-google-audiences. Sinon, définissez audience sur https://SERVICE_NAME, où SERVICE_NAME est le nom du service Endpoints.
    • expiryLength : délai d'expiration du jeton JWT, en secondes.
    Python
    • sa_keyfile : chemin d'accès complet au fichier de clé privée du compte de service.
    • sa_email : adresse e-mail du compte de service.
    • audience : si vous avez ajouté le champ x-google-audiences au document OpenAPI, définissez audience sur l'une des valeurs que vous avez spécifiées pour x-google-audiences. Sinon, définissez audience sur https://SERVICE_NAME, où SERVICE_NAME est le nom du service Endpoints.
    • expiry_length : délai d'expiration du jeton JWT, en secondes.
    Go
    • saKeyfile : chemin d'accès complet au fichier de clé privée du compte de service.
    • saEmail : adresse e-mail du compte de service.
    • audience : si vous avez ajouté le champ x-google-audiences au document OpenAPI, définissez audience sur l'une des valeurs que vous avez spécifiées pour x-google-audiences. Sinon, définissez audience sur https://SERVICE_NAME, où SERVICE_NAME est le nom du service Endpoints.
    • expiryLength : délai d'expiration du jeton JWT, en secondes.

    La fonction crée un jeton JWT, le signe à l'aide du fichier de clé privée et renvoie le jeton JWT signé.

    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: %w", err)
    	}
    	conf, err := google.JWTConfigFromJSON(sa)
    	if err != nil {
    		return "", fmt.Errorf("Could not parse service account JSON: %w", err)
    	}
    	block, _ := pem.Decode(conf.PrivateKey)
    	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    	if err != nil {
    		return "", fmt.Errorf("private key parse error: %w", 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. Dans le service appelant, ajoutez la fonction suivante pour envoyer le jeton JWT signé dans l'en-tête Authorization: Bearer de la requête à l'API :
    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: %w", 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: %w", err)
    	}
    	defer response.Body.Close()
    	responseData, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %w", err)
    	}
    	return string(responseData), nil
    }
    

Par mesure de sécurité, lorsque vous envoyez une requête à l'aide d'un jeton JWT, nous vous recommandons de placer le jeton d'authentification dans l'en-tête Authorization: Bearer. Exemple :

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

ENDPOINTS_HOST et TOKEN sont des variables d'environnement contenant, respectivement, le nom d'hôte de l'API et le jeton d'authentification.

Recevoir les résultats authentifiés dans votre API

ESP transfère généralement tous les en-têtes reçus. Cependant, il remplace l'en-tête Authorization d'origine lorsque l'adresse de backend est spécifiée par x-google-backend dans la spécification OpenAPI ou BackendRule dans la configuration du service gRPC.

ESP envoie le résultat de l'authentification dans le champ X-Endpoint-API-UserInfo à l'API backend. Nous vous recommandons d'utiliser cet en-tête plutôt que l'en-tête Authorization d'origine. Cet en-tête est une chaîne que base64url encode un objet JSON. Le format d'objet JSON diffère entre ESPv2 et ESP. Pour ESPv2, l'objet JSON correspond exactement à la charge utile JWT d'origine. Pour ESP, l'objet JSON utilise des noms de champs différents et place la charge utile JWT d'origine sous le champ claims. Pour en savoir plus sur ce format, consultez la section Gérer les jetons JWT dans le service de backend.

Étapes suivantes