S'authentifier entre différents services

En plus d'authentifier les requêtes des utilisateurs finaux, vous pouvez authentifier les services (utilisateurs non humains) qui envoient des requêtes à votre API. Cette page explique comment utiliser les comptes de service pour fournir une authentification aux humains ou aux services.

Aperçu

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 via un compte de 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 la configuration d'API de votre service API Gateway.
  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.

API Gateway vérifie que les revendications du jeton JWT correspondent à votre configuration d'API avant de transférer la requête à l'API. API Gateway ne vérifie pas les autorisations Cloud Identity que vous avez 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 Google Cloud Console et l'outil de ligne de commande gcloud pour créer le compte de service et le fichier de clé privée, et comment 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.

Pour créer un compte de service avec une clé :

Cloud Console

Créez un compte de service :

  1. Dans Cloud Console, accédez à Créer un compte de service.

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

  2. Sélectionnez un projet.

  3. Dans le champ Nom du compte de service, saisissez un nom. Cloud Console remplit le champ ID du compte de service en fonction de ce nom.

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

  5. Cliquez sur Create (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 Continuer.

  8. Cliquez sur OK pour terminer la création du compte de service.

    Ne fermez pas la fenêtre de votre navigateur. Vous en aurez besoin pour la procédure suivante.

Créez une clé de compte de service :

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

gcloud

Vous pouvez exécuter les commandes suivantes à l'aide du SDK Cloud 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

Lorsque vous créez une configuration d'API pour votre passerelle, vous spécifiez un compte de service que celle-ci utilise pour interagir avec d'autres services. Pour activer l'authentification des comptes de service pour les services appellant votre passerelle, modifiez l'objet d'exigence de sécurité et l'objet de définitions de sécurité dans votre configuration d'API. Les étapes ci-dessous permettent à API Gateway de valider les revendications dans le jeton JWT signé utilisé par les services appelants.

  1. Ajoutez le compte de service en tant qu'émetteur dans votre configuration d'API.

    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 la configuration d'API, 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, API Gateway exige que la revendication "aud" (audience) du jeton JWT soit au format https://SERVICE_NAME, où SERVICE_NAME est le nom du service API Gateway, que vous avez configuré dans le champ host du document OpenAPI.

  3. 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: []
      
  4. Déployez votre configuration d'API mise à jour.

Avant qu'API Gateway ne transfère une requête à votre API, API Gateway 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 de votre configuration d'API ;
  • 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 API Gateway 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 et x-google-audiences, consultez la page Extensions OpenAPI.

Effectuer une requête authentifiée à une API d'API Gateway

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 la configuration d'API 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. Ajoutez la fonction suivante au service appelant 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 à votre configuration d'API, 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 de votre service API Gateway.
    • 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 à votre configuration d'API, 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 de votre service API Gateway.
    • 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 à votre configuration d'API, 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 de votre service API Gateway.
    • 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: %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. 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: %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
    }
    

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}" \
  "${GATEWAY_URL}/echo"

GATEWAY_URL et TOKEN sont des variables d'environnement contenant, respectivement, l'URL de la passerelle déployée et le jeton d'authentification.

Recevoir les résultats authentifiés dans votre API

API Gateway 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 configuration d'API.

API Gateway envoie le résultat de l'authentification dans le champ X-Apigateway-Api-Userinfo à l'API backend. Il est recommandé d'utiliser cet en-tête à la place de l'en-tête Authorization d'origine. Cet en-tête est encodé en base64url et contient la charge utile JWT.

Étape suivante