Autenticación entre servicios

Además de autenticar usuarios, es posible que debas permitir que otros servicios interactúen con tu API. Si bien las aplicaciones cliente pueden proporcionar a los usuarios un mensaje de acceso web a fin de que envíen sus credenciales, necesitas otro enfoque para una comunicación segura entre los servicios. En esta página, se muestra el enfoque que recomendamos para implementar la autenticación entre servicios y se proporciona un código de muestra.

Descripción general

Para identificar un servicio que envía solicitudes a tu API, usas una cuenta de servicio. El servicio de llamadas usa la clave privada de la cuenta de servicio para firmar un token web JSON (JWT) seguro y enviarlo en la solicitud a tu API.

Para implementar la autenticación entre servicios en tu API y en el servicio de llamadas, completa los pasos siguientes:

  1. Crea una cuenta de servicio y una clave para que use el servicio de llamadas.
  2. Agrega asistencia de autenticación en el documento de OpenAPI para tu servicio de Cloud Endpoints.
  3. Agrega código al servicio de llamadas que realiza las tareas siguientes:

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

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

Requisitos previos

En esta página, se supone que ya:

Crea una cuenta de servicio con una clave

Necesitas una cuenta de servicio con un archivo de claves privadas que el servicio de llamadas usa para firmar el JWT. Si tienes más de un servicio que envía solicitudes a tu API, puedes crear una cuenta que represente todos los servicios de llamadas. Si necesitas diferenciar entre los servicios (por ejemplo, podrían tener permisos diferentes), puedes crear una cuenta de servicio y una clave para cada servicio de llamadas.

En esta sección, se muestra cómo usar la consola de Google Cloud y la herramienta de línea de comandos de gcloud para crear la cuenta de servicio y el archivo de clave privada, y para asignar a la cuenta de servicio el rol de Creador de tokens de cuenta de servicio. Si deseas obtener más información para usar una API y realizar esta tarea, consulta Crea y administra cuentas de servicio.

Para crear una cuenta de servicio y una clave, haz lo siguiente:

Consola de Google Cloud

  1. Crea una cuenta de servicio:

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

      Ve a la página Crear cuenta de servicio

    2. Selecciona el proyecto que deseas usar.

    3. Ingresa un nombre en el campo Nombre de cuenta de servicio.

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

    5. Haz clic en Crear.

    6. Haz clic en Listo.

      No cierres la ventana del navegador. la usarás en el próximo paso.

  2. Para crear una clave de cuenta de servicio, haz lo siguiente:

    1. En la consola de Google Cloud , haz clic en la dirección de correo electrónico de la cuenta de servicio que creaste.
    2. Haz clic en Claves.
    3. Haz clic en Agregar clave -> Crear nueva clave.
    4. Haz clic en Crear. Se descarga en tu computadora 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 Google Cloud CLI en tu máquina local o en Cloud Shell.

  1. Configura la cuenta predeterminada para gcloud. Si tienes más de una cuenta, asegúrate de elegir la que está en el Google Cloud proyecto que deseas usar.

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

    gcloud projects list
    
  3. Configura el proyecto predeterminado. Reemplaza PROJECT_ID por el Google Cloud ID del proyecto que deseas usar.

    gcloud config set project PROJECT_ID
  4. Crea una cuenta de servicio. Reemplaza SA_NAME y SA_DISPLAY_NAME por el nombre y el nombre comercial que deseas usar.

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

    gcloud iam service-accounts list
    
  6. Agrega la función Creador de tokens de cuenta de servicio. Reemplaza SA_EMAIL_ADDRESS por la dirección de correo electrónico 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 claves de cuenta de servicio en el directorio de trabajo actual. Reemplaza FILE_NAME por el nombre que deseas usar para el archivo de claves. De forma predeterminada, el comando de 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.

Si deseas obtener información sobre cómo proteger la clave privada, consulta Recomendaciones para administrar las credenciales.

Configura tu API para que sea compatible con la autenticación

Para habilitar la autenticación de cuentas de servicio para los servicios que llaman a tu puerta de enlace, modifica los objetos de seguridad en tu documento de OpenAPI para que el ESP valide las reclamaciones en el JWT firmado. Las modificaciones variarán según la versión de la especificación de OpenAPI que se use.

OpenAPI 2.0

  1. Agrega la cuenta de servicio como una entidad emisora 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"
      
    • Reemplaza DEFINITION_NAME por una cadena que identifique esta definición de seguridad. Es posible que desees reemplazarlo por el nombre de la cuenta de servicio o un nombre que identifique el servicio de llamadas.
    • Reemplaza SA_EMAIL_ADDRESS por la dirección de correo electrónico de la cuenta de servicio.
    • Puedes definir varias definiciones de seguridad en tu especificación de OpenAPI, pero cada definición debe tener un x-google-issuer diferente. Si creaste cuentas de servicio separadas para cada servicio de llamadas, puedes crear una definición de seguridad por 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. O bien agrega x-google-audiences a la sección securityDefinitions. Si no agregas x-google-audiences, el ESP requiere que la reclamación "aud" (público) en el JWT tenga el formato https://SERVICE_NAME, en el que SERVICE_NAME es el nombre de tu servicio de ESP, que configuraste en el campo host de tu documento de OpenAPI.
  3. Agrega una sección security al nivel superior del archivo (sin sangría ni anidado) para aplicarla en toda la API, o al nivel de los métodos para aplicarla a un método específico. Si usas las secciones security en los niveles de la API y de los métodos, la configuración del nivel de los métodos anula la configuración del nivel de la API.
    security:
      - DEFINITION_NAME: []
    • Reemplaza DEFINITION_NAME por el nombre que usaste en la sección securityDefinitions.
    • Si tienes más de una definición en la sección securityDefinitions, agrégalas en la sección security, por ejemplo:
      security:
        - service-1: []
        - service-2: []
  4. Implementa tu especificación de OpenAPI actualizada. Antes de que el ESP reenvíe una solicitud a tu API, verifica lo siguiente:
    • La firma del JWT con 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) en el JWT coincida con el valor especificado en el campo x-google-issuer
    • Que la reclamación "aud"(público) en el JWT contenga el nombre de tu servicio de ESP o coincida con uno de los valores que especificaste en el campo x-google-audiences
    • Que el token no haya vencido mediante la reclamación "exp"(hora de vencimiento)

OpenAPI 3.x

  1. Agrega la cuenta de servicio como una entidad emisora 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: []
    • Reemplaza SCHEME_NAME por una cadena que identifique este esquema de seguridad. Es posible que desees reemplazarlo por el nombre de la cuenta de servicio o un nombre que identifique el servicio de llamadas.
    • Reemplaza SA_EMAIL_ADDRESS por la dirección de correo electrónico 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 creaste cuentas de servicio separadas para cada servicio de llamadas, puedes crear una definición de seguridad por 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. O bien agrega audiences a la sección securitySchemes. Si no agregas audiences, el ESP requiere que la reclamación "aud" (público) en el JWT tenga el formato https://SERVICE_NAME, en el que SERVICE_NAME es el nombre de tu servicio de ESP, que configuraste en el campo host de tu documento de OpenAPI.
  3. Agrega una sección security al nivel superior del archivo (sin sangría ni anidado) para aplicarla en toda la API, o al nivel de los métodos para aplicarla a un método específico. Si usas las secciones security en los niveles de la API y de los métodos, la configuración del nivel de los métodos anula la configuración del nivel de la API.
    security:
      - SCHEME_NAME: []
    • Reemplaza SCHEME_NAME por el nombre que usaste en la sección securitySchemes.
    • Si tienes más de una definición en la sección securitySchemes, agrégalas en la sección security, por ejemplo:
      security:
        - service-1: []
        - service-2: []
  4. Implementa tu especificación de OpenAPI actualizada. Antes de que el ESP reenvíe una solicitud a tu API, verifica lo siguiente:
    • La firma del JWT con 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) en el JWT coincida con el valor especificado en el campo issuer
    • Que la reclamación "aud"(público) en el JWT contenga el nombre de tu servicio de ESP o coincida con uno de los valores que especificaste en el campo audiences
    • Que el token no haya vencido mediante la reclamación "exp"(hora de vencimiento)

Realiza una solicitud autenticada a una API de Endpoints

Para realizar una solicitud autenticada, el servicio de llamadas envía un JWT firmado por la cuenta de servicio que especificaste en el documento de OpenAPI. El servicio de llamadas debe hacer lo siguiente:

  1. Crear un JWT y firmarlo con la clave privada de la cuenta de servicio
  2. Enviar el JWT firmado en una solicitud a la API

En el siguiente código de muestra, se muestra este proceso para los lenguajes seleccionados. Si deseas realizar una solicitud autenticada en otros lenguajes, consulta jwt.io para obtener una lista de bibliotecas compatibles.

  1. En el servicio de llamadas, agrega la función que sigue y pásala a los parámetros siguientes:
    Java
    • saKeyfile: La ruta de acceso completa al archivo de claves privadas de la cuenta de servicio
    • saEmail: La dirección de correo electrónico de la cuenta de servicio
    • audience: Si agregaste el campo x-google-audiences a tu documento de OpenAPI, configura audience como uno de los valores que especificaste para x-google-audiences. De lo contrario, configura audience como https://SERVICE_NAME, en el que SERVICE_NAME es el nombre de tu servicio de Endpoints
    • expiryLength: El tiempo de vencimiento del JWT, en segundos
    Python
    • sa_keyfile: La ruta de acceso completa al archivo de claves privadas de la cuenta de servicio
    • sa_email: La dirección de correo electrónico de la cuenta de servicio
    • audience: Si agregaste el campo x-google-audiences a tu documento de OpenAPI, configura audience como uno de los valores que especificaste para x-google-audiences. De lo contrario, configura audience como https://SERVICE_NAME, en el que SERVICE_NAME es el nombre de tu servicio de Endpoints
    • expiry_length: El tiempo de vencimiento del JWT, en segundos
    Go
    • saKeyfile: La ruta de acceso completa al archivo de claves privadas de la cuenta de servicio
    • saEmail: La dirección de correo electrónico de la cuenta de servicio
    • audience: Si agregaste el campo x-google-audiences a tu documento de OpenAPI, configura audience como uno de los valores que especificaste para x-google-audiences. De lo contrario, configura audience como https://SERVICE_NAME, en el que SERVICE_NAME es el nombre de tu servicio de Endpoints
    • expiryLength: El tiempo de vencimiento del JWT, en segundos

    La función crea un JWT, lo firma con el archivo de claves privadas y muestra 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
    
    
    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 := 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 llamadas, agrega la función siguiente 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()
    
    
    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 := io.ReadAll(response.Body)
    	if err != nil {
    		return "", fmt.Errorf("failed to parse HTTP response: %w", err)
    	}
    	return string(responseData), nil
    }
    

Cuando envíes una solicitud con un JWT, por motivos de seguridad, te recomendamos que coloques 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 tu API y el token de autenticación, respectivamente.

Recibe resultados autenticados en tu API

Por lo general, el ESP reenvía todos los encabezados que recibe. Sin embargo, anula el encabezado original Authorization cuando la dirección de backend se especifique mediante x-google-backend en la especificación de OpenAPI o BackendRule en la configuración del servicio de gRPC.

El ESP enviará el resultado de la autenticación en X-Endpoint-API-UserInfo a la API de 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 difiere entre ESPv2 y ESP. En el caso de ESPv2, el objeto JSON es exactamente la carga útil original del JWT. En el caso del ESP, el objeto JSON usa nombres de campos diferentes y coloca la carga útil original del JWT en el campo claims. Consulta Controla JWT en el servicio de backend para obtener más información sobre el formato.

¿Qué sigue?