Autenticación entre servicios

Además de autenticar a los usuarios, es posible que tengas que permitir que otros servicios interactúen con tu API. Aunque las aplicaciones cliente pueden proporcionar a los usuarios una petición de inicio de sesión web para que envíen sus credenciales, necesitas otro método para establecer una comunicación segura entre servicios. En esta página se muestra el enfoque que recomendamos para implementar la autenticación entre servicios y se proporciona código de ejemplo.

Información general

Para identificar un servicio que envía solicitudes a tu API, debes usar una cuenta de servicio. El servicio de llamada usa la clave privada de la cuenta de servicio para firmar un JSON Web Token (JWT) seguro y envía el JWT firmado en la solicitud a tu API.

Para implementar la autenticación entre servicios en tu API y en el servicio de llamada, sigue estos pasos:

  1. Crea una cuenta de servicio y una clave para que las use el servicio de llamada.
  2. Añade compatibilidad con la autenticación en el documento OpenAPI de tu servicio de Cloud Endpoints.
  3. Añade código al servicio de llamadas que haga lo siguiente:

    • Crea un JWT y lo firma con la clave privada de la cuenta de servicio.
    • Envía el JWT firmado en una solicitud a la API.

ESP valida que las reclamaciones del JWT coincidan con la configuración de tu documento OpenAPI antes de reenviar la solicitud a tu API. ESP no comprueba los permisos de Cloud Identity que hayas concedido en la cuenta de servicio.

Requisitos previos

En esta página se da por hecho que ya has hecho lo siguiente:

Crear una cuenta de servicio con una clave

Necesitas una cuenta de servicio con un archivo de clave privada que el servicio de llamada utilice para firmar el JWT. Si tienes más de un servicio que envía solicitudes a tu API, puedes crear una cuenta de servicio que represente a todos los servicios que hacen llamadas. Si necesitas diferenciar entre los servicios (por ejemplo, si tienen permisos diferentes), puedes crear una cuenta de servicio y una clave para cada servicio de llamada.

En esta sección se muestra cómo usar la consola de Google Cloud y la herramienta de línea de comandos gcloud para crear la cuenta de servicio y el archivo de clave privada, así como para asignar a la cuenta de servicio el rol Creador de tokens de cuenta de servicio. Para obtener información sobre cómo usar una API para llevar a cabo esta tarea, consulta el artículo Crear y gestionar cuentas de servicio.

Para crear una cuenta de servicio y una clave, sigue estos pasos:

Google Cloud console

  1. Crea una cuenta de servicio:

    1. En la Google Cloud consola, ve a la página Crear cuenta de servicio.

      Ir a la página Crear cuenta de servicio

    2. Selecciona el proyecto que quieras usar.

    3. En el campo Nombre de cuenta de servicio, escribe un nombre.

    4. Opcional: En el campo Descripción de la cuenta de servicio, escribe una descripción.

    5. Haz clic en Crear.

    6. Haz clic en Listo.

      No cierres la ventana del navegador. Lo usarás en el siguiente paso.

  2. Crea una clave de cuenta de servicio:

    1. En la Google Cloud consola, haz clic en la dirección de correo de la cuenta de servicio que has creado.
    2. Haz clic en Claves.
    3. Haz clic en Añadir clave y, a continuación, en Crear clave.
    4. Haz clic en Crear. Se descargará en tu ordenador un archivo JSON que contiene la clave privada de la cuenta de servicio.
    5. Haz clic en Cerrar.

gcloud

Puedes ejecutar los siguientes comandos con la CLI de Google Cloud en tu máquina local o en Cloud Shell.

  1. Define la cuenta predeterminada de gcloud. Si tienes más de una cuenta, asegúrate de elegir la cuenta del Google Cloud proyecto que quieras usar.

    gcloud auth login
    
  2. Muestra los IDs de tus Google Cloud proyectos.

    gcloud projects list
    
  3. Define el proyecto predeterminado. Sustituye PROJECT_ID por el ID del proyecto que quieras usar. Google Cloud

    gcloud config set project PROJECT_ID
  4. Crea una cuenta de servicio. Sustituye SA_NAME y SA_DISPLAY_NAME por el nombre y el nombre visible que quieras usar.

    gcloud iam service-accounts create SA_NAME \
      --display-name "SA_DISPLAY_NAME"
  5. Se muestra la dirección de correo de la cuenta de servicio que acabas de crear.

    gcloud iam service-accounts list
    
  6. Añade el rol Creador de tokens de cuenta de servicio. Sustituye SA_EMAIL_ADDRESS por la dirección de correo de la cuenta de servicio.

    gcloud projects add-iam-policy-binding PROJECT_ID \
      --member serviceAccount:SA_EMAIL_ADDRESS \
      --role roles/iam.serviceAccountTokenCreator
  7. Crea un archivo de clave de cuenta de servicio en el directorio de trabajo actual. Sustituye FILE_NAME por el nombre que quieras usar para el archivo de claves. De forma predeterminada, el comando gcloud crea un archivo JSON.

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

Consulta la referencia de gcloud para obtener más información sobre los comandos anteriores.

Para obtener información sobre cómo proteger la clave privada, consulta las prácticas recomendadas para gestionar las credenciales.

Configurar la API para que admita la autenticación

Para habilitar la autenticación de cuentas de servicio en los servicios que llaman a tu pasarela, modifica los objetos de seguridad de tu documento OpenAPI para que ESP valide las reclamaciones del JWT firmado. Las modificaciones variarán en función de la versión de la especificación de OpenAPI que se utilice.

OpenAPI 2.0

  1. Añade la cuenta de servicio como emisor en tu especificación de 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"
      
    • Sustituye DEFINITION_NAME por una cadena que identifique esta definición de seguridad. Puede que quieras sustituirlo por el nombre de la cuenta de servicio o por un nombre que identifique el servicio que llama.
    • Sustituye SA_EMAIL_ADDRESS por la dirección de correo de la cuenta de servicio.
    • Puedes definir varias definiciones de seguridad en tu especificación de OpenAPI, pero cada una debe tener un x-google-issuer diferente. Si has creado cuentas de servicio independientes para cada servicio de llamadas, puedes crear una definición de seguridad para cada cuenta de servicio. Por ejemplo:
      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. Si quieres, añade x-google-audiences a la sección securityDefinitions. Si no añades x-google-audiences, el ESP requiere que la reclamación "aud" (audiencia) del JWT tenga el formato https://SERVICE_NAME, donde SERVICE_NAME es el nombre de tu servicio de ESP, que has configurado en el campo host de tu documento OpenAPI.
  3. Añade una sección security en el nivel superior del archivo (sin sangría ni anidación) para aplicarla a toda la API, o en el nivel del método para aplicarla a un método específico. Si usas secciones security tanto a nivel de API como a nivel de método, los ajustes a nivel de método anulan los ajustes a nivel de API.
    security:
      - DEFINITION_NAME: []
    • Sustituye DEFINITION_NAME por el nombre que hayas usado en la sección securityDefinitions.
    • Si tienes más de una definición en la sección securityDefinitions, añádelas en la sección security. Por ejemplo:
      security:
        - service-1: []
        - service-2: []
  4. Despliega tu especificación de OpenAPI actualizada. Antes de que ESP reenvíe una solicitud a tu API, ESP verifica lo siguiente:
    • La firma del JWT mediante la clave pública, que se encuentra en el URI especificado en el campo x-google-jwks_uri de tu especificación de OpenAPI.
    • Que la reclamación "iss"(emisor) del JWT coincida con el valor especificado en el campo x-google-issuer.
    • Que la reclamación "aud"(audiencia) del JWT contenga el nombre de su servicio de ESP o coincida con uno de los valores que haya especificado en el campo x-google-audiences.
    • Que el token no haya caducado mediante la reclamación "exp"(hora de vencimiento).

OpenAPI 3.x

  1. Añade la cuenta de servicio como emisor en tu especificación de OpenAPI:
    components:
      securitySchemes:
        SCHEME_NAME:
          type: oauth2
          flows:
           implicit:
             authorizationUrl: ""
             scopes: {}
          x-google-auth:
            issuer: SA_EMAIL_ADDRESS
            jwksUri: https://www.googleapis.com/robot/v1/metadata/x509/SA_EMAIL_ADDRESS
            audiences:
              - 848149964201.apps.googleusercontent.com
              - 841077041629.apps.googleusercontent.com
            jwtLocations:
              - header: Authorization
                valuePrefix: "Bearer "
    security:
      - SCHEME_NAME: []
    • Sustituye SCHEME_NAME por una cadena que identifique este esquema de seguridad. Puede que quieras sustituirlo por el nombre de la cuenta de servicio o por un nombre que identifique el servicio que hace la llamada.
    • Sustituye SA_EMAIL_ADDRESS por la dirección de correo de la cuenta de servicio.
    • Puedes definir varios esquemas de seguridad en tu especificación de OpenAPI, pero cada definición debe tener un issuer diferente. Si has creado cuentas de servicio independientes para cada servicio de llamadas, puedes crear una definición de seguridad para cada cuenta de servicio. Por ejemplo:
      components:
        securitySchemes:
          service-1:
            type: oauth2
            flows:
             implicit:
               authorizationUrl: ""
               scopes: {}
            x-google-auth:
              issuer: "service-1@example-project-12345.iam.gserviceaccount.com"
              jwksUri: https://www.googleapis.com/robot/v1/metadata/x509/service-1@example-project-12345.iam.gserviceaccount.com
              jwtLocations:
                - header: Authorization
                  valuePrefix: "Bearer "
          service-2:
            type: oauth2
            flows:
             implicit:
               authorizationUrl: ""
               scopes: {}
            x-google-auth:
              issuer: "service-2@example-project-12345.iam.gserviceaccount.com"
              jwksUri: "https://www.googleapis.com/robot/v1/metadata/x509/service-2@example-project-12345.iam.gserviceaccount.com"
              jwtLocations:
                - header: Authorization
                  valuePrefix: "Bearer "
  2. Si quieres, añade audiences a la sección securitySchemes. Si no añades audiences, el ESP requiere que la reclamación "aud" (audiencia) del JWT tenga el formato https://SERVICE_NAME, donde SERVICE_NAME es el nombre de tu servicio ESP, que has configurado en el campo host de tu documento OpenAPI.
  3. Añade una sección security en el nivel superior del archivo (sin sangría ni anidación) para aplicarla a toda la API, o en el nivel del método para aplicarla a un método específico. Si usas secciones security tanto a nivel de API como a nivel de método, los ajustes a nivel de método anulan los ajustes a nivel de API.
    security:
      - SCHEME_NAME: []
    • Sustituye SCHEME_NAME por el nombre que hayas usado en la sección securitySchemes.
    • Si tienes más de una definición en la sección securitySchemes, añádelas en la sección security. Por ejemplo:
      security:
        - service-1: []
        - service-2: []
  4. Despliega tu especificación de OpenAPI actualizada. Antes de que ESP reenvíe una solicitud a tu API, ESP verifica lo siguiente:
    • La firma del JWT mediante la clave pública, que se encuentra en el URI especificado en el campo jwksUri de tu especificación de OpenAPI.
    • Que la reclamación "iss"(emisor) del JWT coincida con el valor especificado en el campo issuer.
    • Que la reclamación "aud"(audiencia) del JWT contenga el nombre de su servicio de ESP o coincida con uno de los valores que haya especificado en el campo audiences.
    • Que el token no haya caducado mediante la reclamación "exp"(hora de vencimiento).

Hacer una solicitud autenticada a una API de Endpoints

Para hacer una solicitud autenticada, el servicio que llama envía un JWT firmado por la cuenta de servicio que has especificado en el documento OpenAPI. El servicio de llamadas debe cumplir los siguientes requisitos:

  1. Crea un JWT y fírmalo con la clave privada de la cuenta de servicio.
  2. Envía el JWT firmado en una solicitud a la API.

El siguiente código de ejemplo muestra este proceso para algunos idiomas. Para hacer una solicitud autenticada en otros idiomas, consulta jwt.io para ver una lista de bibliotecas compatibles.

  1. En el servicio de llamada, añade la siguiente función y pásale los parámetros que se indican a continuación:
    Java
    • saKeyfile: ruta completa al archivo de clave privada de la cuenta de servicio.
    • saEmail: la dirección de correo de la cuenta de servicio.
    • audience: Si ha añadido el campo x-google-audiences a su documento de OpenAPI, asigne a audience uno de los valores que haya especificado para x-google-audiences. De lo contrario, asigna el valor https://SERVICE_NAME a audience, donde SERVICE_NAME es el nombre del servicio Endpoints.
    • expiryLength: tiempo de vencimiento del JWT, en segundos.
    Python
    • sa_keyfile: ruta completa al archivo de clave privada de la cuenta de servicio.
    • sa_email: la dirección de correo de la cuenta de servicio.
    • audience: Si has añadido el campo x-google-audiences a tu documento de OpenAPI, asigna a audience uno de los valores que hayas especificado para x-google-audiences. De lo contrario, asigna el valor https://SERVICE_NAME a audience, donde SERVICE_NAME es el nombre del servicio Endpoints.
    • expiry_length: tiempo de vencimiento del JWT, en segundos.
    Ir
    • saKeyfile: ruta completa al archivo de clave privada de la cuenta de servicio.
    • saEmail: la dirección de correo de la cuenta de servicio.
    • audience: Si ha añadido el campo x-google-audiences a su documento de OpenAPI, asigne a audience uno de los valores que haya especificado para x-google-audiences. De lo contrario, asigna el valor https://SERVICE_NAME a audience, donde SERVICE_NAME es el nombre del servicio Endpoints.
    • expiryLength: tiempo de vencimiento del JWT, en segundos.

    La función crea un JWT, lo firma con el archivo de clave privada y devuelve el JWT firmado.

    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
    
    
    Ir
    
    // 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 := os.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. En el servicio de llamada, añade la siguiente función para enviar el JWT firmado en el encabezado Authorization: Bearer de la solicitud a la 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()
    
    
    Ir
    
    // 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 := io.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %w", err)
    	}
    	return string(responseData), nil
    }
    

Cuando envías una solicitud mediante un JWT, por motivos de seguridad, te recomendamos que incluyas el token de autenticación en el encabezado Authorization: Bearer. Por ejemplo:

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

donde ENDPOINTS_HOST y TOKEN son variables de entorno que contienen el nombre de host de la API y el token de autenticación, respectivamente.

Recibir resultados autenticados en tu API

Normalmente, los ESP reenvían todos los encabezados que reciben. Sin embargo, anula el encabezado Authorization original cuando la dirección del backend se especifica mediante x-google-backend en la especificación de OpenAPI o BackendRule en la configuración del servicio gRPC.

El ESP enviará el resultado de la autenticación en el X-Endpoint-API-UserInfo a la API backend. Te recomendamos que uses este encabezado en lugar del encabezado Authorization original. Este encabezado es una cadena que base64url codifica un objeto JSON. El formato del objeto JSON es diferente en ESPv2 y ESP. En ESPv2, el objeto JSON es exactamente la carga útil del JWT original. En el caso de los ESPs, el objeto JSON usa nombres de campo diferentes y coloca la carga útil original del JWT en el campo claims. Consulta Gestionar JWTs en el servicio backend para obtener más información sobre el formato.

Siguientes pasos