Autenticazione tra servizi

Oltre ad autenticare gli utenti, potresti dover consentire ad altri servizi di interagire con la tua API. Sebbene le applicazioni client possano fornire agli utenti una richiesta di accesso web per inviare le loro credenziali, è necessario un altro approccio per la comunicazione sicura tra servizi. Questa pagina mostra l'approccio consigliato per implementare l'autenticazione tra i servizi e fornisce codice campione.

Panoramica

Per identificare un servizio che invia richieste alla tua API, utilizzi un account di servizio. Il servizio chiamante utilizza la chiave privata dell'account di servizio per firmare un token web JSON (JWT) sicuro e invia il JWT firmato nella richiesta all'API.

Per implementare l'autenticazione da un servizio all'altro nell'API e nel servizio di chiamata:

  1. Crea un account di servizio e una chiave da utilizzare per il servizio di chiamata.
  2. Aggiungi il supporto per l'autenticazione nel documento OpenAPI per il tuo servizio Cloud Endpoints.
  3. Aggiungi al servizio di chiamata il codice che:

    • Crea un JWT e lo firma con la chiave privata dell'account di servizio.
    • Invia il JWT firmato in una richiesta all'API.

ESP convalida che i claim nel JWT corrispondano alla configurazione nel documento OpenAPI prima di inoltrare la richiesta all'API. ESP non controlla le autorizzazioni di Cloud Identity che hai concesso all'account di servizio.

Prerequisiti

Questa pagina presuppone che tu abbia già:

Creazione di un account di servizio con una chiave

È necessario un account di servizio con un file della chiave privata utilizzato dal servizio chiamante per firmare il JWT. Se hai più di un servizio che invia richieste alla tua API, puoi creare un account di servizio per rappresentare tutti i servizi di chiamata. Se devi distinguere i servizi, ad esempio perché hanno autorizzazioni diverse, puoi creare un account di servizio e una chiave per ogni servizio di chiamata.

Questa sezione mostra come utilizzare la console Google Cloud e lo strumento a riga di comando gcloud per creare l'account di servizio e il file della chiave privata e assegnare all'account di servizio il ruolo Creatore di token dell'account di servizio. Per informazioni sull'utilizzo di un'API per eseguire questa operazione, consulta Creare e gestire gli account di servizio.

Per creare un account di servizio e una chiave:

Console Google Cloud

  1. Crea un account di servizio:

    1. Nella console Google Cloud, vai alla pagina Crea account di servizio.

      Vai alla pagina Crea account di servizio

    2. Seleziona il progetto che vuoi utilizzare.

    3. Inserisci un nome nel campo Nome account di servizio.

    4. (Facoltativo) Nel campo Descrizione account di servizio, inserisci una descrizione.

    5. Fai clic su Crea.

    6. Fai clic sul campo Seleziona un ruolo. In Tutti i ruoli, seleziona Account di servizio > Creatore token account di servizio.

    7. Fai clic su Fine.

      Non chiudere la finestra del browser. Lo utilizzerai nel passaggio successivo.

  2. Crea una chiave dell'account di servizio:

    1. Nella console Google Cloud, fai clic sull'indirizzo email dell'account di servizio che hai creato.
    2. Fai clic su Chiavi.
    3. Fai clic su Aggiungi chiave, quindi su Crea nuova chiave.
    4. Fai clic su Crea. Sul computer viene scaricato un file JSON contenente la chiave privata dell'account di servizio.
    5. Fai clic su Chiudi.

gcloud

Puoi eseguire i seguenti comandi utilizzando Google Cloud CLI sulla tua macchina locale o in Cloud Shell.

  1. Imposta l'account predefinito per gcloud. Se hai più di un account, assicurati di scegliere quello che si trova nel progetto Google Cloud che vuoi utilizzare.

    gcloud auth login
    
  2. Mostra gli ID progetto per i tuoi progetti Google Cloud.

    gcloud projects list
    
  3. Imposta il progetto predefinito. Sostituisci PROJECT_ID con l'ID progetto Google Cloud che vuoi utilizzare.

    gcloud config set project PROJECT_ID
  4. Crea un account di servizio. Sostituisci SA_NAME e SA_DISPLAY_NAME con il nome e il nome visualizzato che vuoi utilizzare.

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
  5. Viene visualizzato l'indirizzo email dell'account di servizio appena creato.

    gcloud iam service-accounts list
    
  6. Aggiungi il ruolo Creatore token account di servizio. Sostituisci SA_EMAIL_ADDRESS con l'indirizzo email dell'account di servizio.

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/iam.serviceAccountTokenCreator
  7. Crea un file della chiave dell'account di servizio nella directory di lavoro corrente. Sostituisci FILE_NAME con il nome che vuoi utilizzare per il file della chiave. Per impostazione predefinita, il comando gcloud crea un file JSON.

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

Per ulteriori informazioni sui comandi precedenti, consulta la pagina di riferimento gcloud.

Per informazioni sulla salvaguardia della chiave privata, consulta Best practice per la gestione delle credenziali.

Configurare l'API per supportare l'autenticazione

Per consentire a ESP di convalidare i claim nel JWT firmato, nel documento OpenAPI devi avere un oggetto requisito di sicurezza e un oggetto definizioni di sicurezza.

  1. Aggiungi l'account di servizio come emittente nel documento 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"
    
    • Sostituisci DEFINITION_NAME con una stringa che identifica questa definizione di sicurezza. Ti consigliamo di sostituirlo con il nome dell'account di servizio o con un nome che identifichi il servizio di chiamata.
    • Sostituisci SA_EMAIL_ADDRESS con l'indirizzo email dell'account di servizio.
    • Puoi definire più definizioni di sicurezza nel documento OpenAPI, ma ogni definizione deve avere un x-google-issuer diverso. Se hai creato account di servizio separati per ogni servizio di chiamata, puoi creare una definizione di sicurezza per ogni account di servizio, ad esempio:
    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. Se vuoi, aggiungi x-google-audiences alla sezione securityDefinitions. Se non aggiungi x-google-audiences, ESP richiede che l'attributo "aud" (segmento di pubblico) nel JWT sia nel formato https://SERVICE_NAME, dove SERVICE_NAME è il nome del servizio Endpoints, che hai configurato nel campo host del tuo documento OpenAPI, a meno che non venga utilizzato il flag --disable_jwt_audience_service_name_check. Se il flag viene utilizzato ex-google-audiences non è specificato, il campo JWT aud non viene selezionato.

  3. Se vuoi, aggiungi x-google-jwt-locations alla sezione securityDefinitions. Puoi utilizzare questo valore per definire una posizione JWT personalizzata. Le posizioni predefinite dei JWT sono l'intestazione Authorization (con prefisso "Bearer "), l'intestazione X-Goog-Iap-Jwt-Assertion o il parametro di query access_token. Nota:

    • Se specifichi x-google-jwt-locations, Endpoints ignora tutte le posizioni predefinite.
    • x-google-jwt-locations è supportato solo da ESPv2.
  4. Aggiungi una sezione security a livello di primo livello del file (non rientrata o nidificata) per applicarla all'intera API o a livello di metodo per applicarla a un metodo specifico. Se utilizzi sezioni security sia a livello di API sia a livello di metodo, le impostazioni a livello di metodo sostituiscono quelle a livello di API.

    security:
      - DEFINITION_NAME: []
    • Sostituisci DEFINITION_NAME con il nome utilizzato nella sezione securityDefinitions.
    • Se hai più di una definizione nella sezione securityDefinitions, aggiungile nella sezione security, ad esempio:

      security:
        - service-1: []
        - service-2: []
      
  5. Esegui il deployment del documento OpenAPI aggiornato. Sostituisci OPENAPI_DOC con il nome del tuo documento OpenAPI.

    gcloud endpoints services deploy OPENAPI_DOC

Prima di inoltrare una richiesta alla tua API, ESP verifica:

  • La firma del JWT utilizzando la chiave pubblica, che si trova all'URI specificato nel campo x-google-jwks_uri del documento OpenAPI.
  • Che l'affermazione "iss" (issuer) nel JWT corrisponda al valore specificato nel x-google-issuer campo.
  • Che l'affermazione "aud" (segmento di pubblico) nel JWT contenga il nome del servizio Endpoints o corrisponda a uno dei valori specificati nel campo x-google-audiences.
  • Che il token non sia scaduto utilizzando l'affermazione "exp" (data e ora di scadenza).

Per ulteriori informazioni su x-google-issuer, x-google-jwks_uri, x-google-audiences e x-google-jwt-locations, consulta le estensioni OpenAPI.

Invio di una richiesta autenticata a un'API Endpoints

Per effettuare una richiesta autenticata, il servizio chiamante invia un JWT firmato dall'account di servizio specificato nel documento OpenAPI. Il servizio di chiamata deve:

  1. Crea un JWT e firmalo con la chiave privata dell'account di servizio.
  2. Invia il JWT firmato in una richiesta all'API.

Il seguente codice campione mostra questa procedura per alcune lingue. Per effettuare una richiesta autenticata in altre lingue, consulta jwt.io per un elenco delle librerie supportate.

  1. Nel servizio chiamante, aggiungi la seguente funzione e passa i seguenti parametri:
    Java
    • saKeyfile: il percorso completo del file della chiave privata dell'account di servizio.
    • saEmail: l'indirizzo email dell'account di servizio.
    • audience: se hai aggiunto il campo x-google-audiences al tuo documento OpenAPI, imposta audience su uno dei valori specificati per x-google-audiences. In caso contrario, imposta audience su https://SERVICE_NAME, dove SERVICE_NAME è il nome del servizio Endpoints.
    • expiryLength: la data di scadenza del JWT, in secondi.
    Python
    • sa_keyfile: il percorso completo del file della chiave privata dell'account di servizio.
    • sa_email: l'indirizzo email dell'account di servizio.
    • audience: se hai aggiunto il campo x-google-audiences al tuo documento OpenAPI, imposta audience su uno dei valori specificati per x-google-audiences. In caso contrario, imposta audience su https://SERVICE_NAME, dove SERVICE_NAME è il nome del servizio Endpoints.
    • expiry_length: la data di scadenza del JWT, in secondi.
    Vai
    • saKeyfile: il percorso completo del file della chiave privata dell'account di servizio.
    • saEmail: l'indirizzo email dell'account di servizio.
    • audience: se hai aggiunto il campo x-google-audiences al tuo documento OpenAPI, imposta audience su uno dei valori specificati per x-google-audiences. In caso contrario, imposta audience su https://SERVICE_NAME, dove SERVICE_NAME è il nome del servizio Endpoints.
    • expiryLength: la data di scadenza del JWT, in secondi.

    La funzione crea un JWT, lo firma utilizzando il file della chiave privata e restituisce il JWT firmato.

    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
    
    
    Vai
    
    // 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. Nel servizio chiamante, aggiungi la seguente funzione per inviare il JWT firmato nell'intestazione Authorization: Bearer nella richiesta all'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()
    
    
    Vai
    
    // 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
    }
    

Quando invii una richiesta utilizzando un JWT, per motivi di sicurezza, ti consigliamo di inserire il token di autenticazione nell'Authorization: Bearer header. Ad esempio:

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

dove ENDPOINTS_HOST e TOKEN sono variabili di ambiente contenenti rispettivamente il nome dell'host dell'API e il token di autenticazione.

Ricevere risultati autenticati nell'API

In genere, l'ESP inoltra tutte le intestazioni che riceve. Tuttavia, sostituisce l'Authorization originale quando l'indirizzo del backend è specificato da x-google-backend nella specifica OpenAPI o da BackendRule nella configurazione del servizio gRPC.

L'ESP invierà il risultato dell'autenticazione in X-Endpoint-API-UserInfo all'API di backend. Ti consigliamo di utilizzare questa intestazione anziché l'intestazioneAuthorization originale. Questa intestazione è una stringa che base64url codifica un oggetto JSON. Il formato dell'oggetto JSON è diverso tra ESPv2 ed ESP. Per ESPv2, l'oggetto JSON è esattamente il payload JWT originale. Per ESP, l'oggetto JSON utilizza nomi di campo diversi e inserisce il payload JWT originale nel campo claims. Per ulteriori informazioni sul formato, consulta Gestire i token JWT nel servizio di backend.

Passaggi successivi