Amplify: GraphQL autorizado por Cognito

amplify

AWS Amplify: GraphQL autorizado por Cognito en una API Lambda — Parte 1

En Amplify, cuando se necesita acceder a tablas de DynamoDB desde Lambda con autenticación basada en Cognito, es decir, grupos de grupos de usuarios o IAM, la tarea es un poco complicada.

Esta publicación detalla la creación de una capa Lambda que puede facilitar este tipo de solicitudes GraphQL basadas en Cognito con facilidad para las funciones Lambda integradas.

Esta publicación muestra cómo crear una capa para solicitudes Lambda GraphQL utilizando la política de IAM de ejecución de la función Lambda, pero no la de la persona que llama.


Está funcionando felizmente, ha creado un esquema GraphQL en AWS Amplify con autenticación basada en grupos de usuarios y está utilizando directivas @auth para limitar el acceso a esos datos (digamos, por ejemplo, que solo el owner autenticado tiene acceso a sus datos) .

Decide que desea agregar una lógica del lado del servidor más compleja en una función Lambda de API REST sin servidor que realiza consultas para un usuario y transforma y devuelve datos en función del resultado.

Pero, por desgracia, después de horas de prueba y error, pronto se queda perplejo, ya que una política de rol de ejecución simple en la función Lambda no tiene el alcance de owner limitado basado en Cognito que necesita, y en su lugar tiene acceso a toda la tabla datos o ninguno.

 

¿Cómo asegurarnos que Lambda ejecute consultas GraphQL internas como si fuera el usuario de Cognito llamando a la función?

La respuesta es simple en teoría, pero requiere algo de magia para que funcione.

 

En este tutorial, analizaremos la creación de una capa de Lambda con un cliente para solicitudes de GraphQL basadas en IAM que se reenvían a través del encabezado de Authorization enviado a la propia función de Lambda.

Nuestro primer paso es inicializar la capa Lambda:

$ amplify add function
? Select which capability you want to add: Lambda layer (shared code & resource used across functions)
? Provide a name for your Lambda layer: layerLambdaGQL
? Choose the runtime that you want to use: NodeJS
? The current AWS account will always have access to this layer.

También queremos ir a la carpeta lib de la capa para instalar las dependencias que usará para solicitudes de GraphQL:

$ cd amplificar/backend/function/<nombre de capa>/lib
$ npm instalar graphql --save
$ npm instalar la etiqueta graphql --save
$ npm url de instalación --guardar
$ npm instalar uuid --guardar

Ahora, construyamos nuestro cliente GraphQL que usarán las funciones de Lambda en esta capa. La lógica aquí es que inicializamos la clase de cliente con un encabezado de Authorization (del formulario IAM Bearer XXXXX) que se almacena como una variable miembro.

Cuando enviamos solicitudes de GraphQL desde el cliente, agregamos ese token de Authorization como el encabezado de autorización sin el portador, lo que hace que la solicitud de la función Lambda funcione como si la hubiera enviado la persona que llamó originalmente.

Guarde el siguiente código para el cliente GraphQL de la capa en amplify/backend/function/<layerName>/opt/graphQLClient.js :

const AWS = require("aws-sdk");
const https = require("https");
const UrlParse = require("url").URL;
const gql = require("graphql-tag");
const graphql = require("graphql");
const { print } = graphql;

/*
 * Waits using an exponential
 * backoff algorithm.
 */
function waitTimeExp(retryCount) {
  return new Promise((resolve) => {
    if (retryCount === 0) {
      resolve();
    } else {
      let MAX_TIME = 20000;
      let waitTime = Math.pow(2, retryCount) * 500;
      let t = Math.min(waitTime, MAX_TIME);

      setTimeout(function () {
        resolve();
      }, t);
    }
  });
}

/**
 * Fetches an HTTP request.
 * @param {*} httpReq An HTTP request to fetch.
 * @returns The body of the HTTP request, parsed as JSON.
 */
function fetchData(httpReq) {
  return new Promise(async (resolve, reject) => {
    let received = "";
    const httpRequest = https
      .request(httpReq, (result) => {
        result
          .on("data", (data) => {
            received += data;
          })
          .on("end", () => {
            let r = JSON.parse(received.toString());
            if (r.hasOwnProperty("errors")) {
              reject(r.errors[0].message);
            } else {
              resolve(r);
            }
          });
      })
      .on("timeout", function () {
        httpRequest.destroy(new Error("timeout"));
      })
      .on("error", (e) => {
        console.log(e);
        reject(e.message);
      });
    httpRequest.write(httpReq.body);
    httpRequest.end();
  });
}

/**
 * Retrieves the GraphQL endpoint from the environment variables.
 * @returns The endpoint of the GraphQL api.
 */
function getEndpoint() {
  let k = Object.keys(process.env).filter((key) => {
    if (key.indexOf("GRAPHQLAPIENDPOINTOUTPUT") !== -1) {
      return true;
    }
    return false;
  });

  let endpoint;
  if (k.length > 0) {
    endpoint = process.env[k[0]];
  }

  return endpoint;
}

/**
 * Manages GraphQL API requests
 */
class GraphQLClient {
  /**
   * Constructs the GraphQLAPI instance with the passed in authorization header
   * from the lambda event and retriving the GraphQL endpoint from env vars.
   * @param {*} authorizationHeader Authorization header of the form Bearer XXX
   */
  constructor(authorizationHeader) {
    this.authorization = authorizationHeader.replace("Bearer ", "");
    this.graphQlEndpoint = getEndpoint();
  }

  /**
   * Builds a GraphQL HTTP request with the authorization configured.
   * @param {*} aQuery A GraphQL query (from graphql/mutations.js, etc.)
   * @param {*} aVariables Input variables to the qu
   * @returns The signed request
   */
  createSignedReq(aQuery, aVariables) {
    const endpoint = new UrlParse(this.graphQlEndpoint).hostname.toString();

    const req = new AWS.HttpRequest(this.graphQlEndpoint, "us-east-1");
    req.method = "POST";
    req.path = "/graphql";
    req.headers.host = endpoint;
    req.headers["Authorization"] = this.authorization;
    req.headers["Content-Type"] = "application/json; charset=UTF-8";

    let queryData = {
      query: print(gql(aQuery)),
    };

    if (aVariables) {
      queryData.variables = aVariables;
    }

    req.body = JSON.stringify(queryData);

    let signedReq = {
      ...req,
      host: endpoint,
      timeout: 30000,
    };
    return signedReq;
  }

  /**
   * Runs a GraphQL query, giving it the proper authorization.
   * @param {*} aQuery The query in GQL.
   * @param {*} aVariables Any variables to pass to the query.
   * @returns The data returned from the GraphQL query.
   */
  async run(aQuery, aVariables) {
    let retries = 0;
    let retry = false;
    let data = null;
    let httpReq = this.createSignedReq(aQuery, aVariables);
    let err = null;
    const MAX_RETRIES = 5;

    do {
      try {
        retry = false;
        await waitTimeExp(retries);
        data = await fetchData(httpReq);
      } catch (error) {
        console.log(error);
        err = error;
        retry = true;
      }
    } while (retry && retries++ < MAX_RETRIES);

    if (retries >= MAX_RETRIES) {
      console.log("Error - max retries");
      throw err;
    }

    return data;
  }

  /**
   * Paginates runs of a GraphQL request, when applicable.
   * @param {*} aQuery The query in GQL.
   * @param {*} aVariables Any variables to pass to the query.
   * @param {*} aQueryName The query name from which to get the items.
   * @param {*} nextToken Next token for the page, if it exists.
   * @param {*} items A running array of all the returned items
   */
  async runWithPaginate(
    aQuery,
    aVariables,
    aQueryName,
    nextToken = null,
    items = []
  ) {
    const { data } = await this.run(aQuery, {
      ...aVariables,
      nextToken: nextToken,
    });
    const page = data[aQueryName];
    items = [...items, ...page.items];
    if (page.nextToken) {
      return this.runWithPaginate(
        aQuery,
        aVariables,
        aQueryName,
        page.nextToken,
        items
      );
    } else {
      return items;
    }
  }
}

module.exports = GraphQLClient;

Entonces, ¿qué hacemos aqui? Esta es una consulta GraphQL bastante general que puede realizar consultas y mutaciones normales, así como consultas paginadas recursivas.

En la construcción, el cliente guarda el endpoint GraphQL de las variables de entorno y la authorization Cognito JWT mencionada anteriormente. Cuando llamamos a run con una consulta y variables, la solicitud HTTP a nuestro punto final de GraphQL se firmará con ese token de autorización y funcionará como si la llamara el usuario al que se hace referencia en ese token.

Eso es todo para la capa Lambda. Simplemente definimos y exportamos este cliente GraphQL para que sea accesible en /opt/graphQLClient.js para las funciones de Lambda en la capa. Si tiene un conjunto común de queries y mutaciones en GQL, también me gusta colocarlas en la capa Lambda, en /opt/graphql/queries.js/opt/graphql/mutations.js, respectivamente.


Próximamente, veremos cómo definir una API REST con funciones Lambda que usan GraphQL y firman solicitudes de los que llama.

En la Parte 2, construiremos la función Lambda para la eliminación en cascada.

Yen la Parte 3 construiremos la API REST y mostraremos el acceso del lado del cliente.

Recent Post