Computación en la Nube
15 min de lectura

Cómo Acceder a Objetos Privados de S3 con AWS Cognito

Guía para proporcionar acceso seguro a archivos privados con la integración de pools de usuarios Cognito, API Gateway, Lambda y S3.

N
Necmettin Demir
21 de julio de 2023
Cargando...

Cómo Acceder a Objetos Privados de S3 con AWS Cognito

AWS Cognito S3
AWS Cognito S3

Escenario

Supongamos que está desarrollando algunas aplicaciones para su cliente. Sin embargo, hay algunos archivos como PDF, Word, Excel, etc. relacionados con los registros de las aplicaciones. Por simplicidad del escenario, supongamos que estos archivos se almacenan en un solo bucket S3 privado en AWS.
Los usuarios necesitan poder acceder a estos archivos relacionados desde el bucket S3 privado a través de un enlace URL en las aplicaciones. Nuestra solución debe funcionar como una solución portátil para cualquier software interno de la empresa.

Introducción

El objetivo de este artículo es mostrar cómo descargar archivos desde un bucket S3 privado utilizando pools de usuarios Cognito. Además de Cognito, se demuestra el flujo desde Cognito hacia API Gateway con Authorizer y la colaboración entre API Gateway y Lambda.
Se han compartido tantas capturas de pantalla como sea posible de la consola de AWS para cada paso. Se han agregado muchas imágenes para hacer los pasos más claros, especialmente para principiantes.

Antecedentes

Para comprender mejor lo desarrollado en este artículo, algunas lecturas previas podrían ser útiles. Los siguientes enlaces serán particularmente útiles para quienes recién comienzan con AWS:

¿Qué Hacer?

Para tal tarea se pueden codificar muchos flujos o métodos. Aquí implementaremos el método mostrado a continuación. Una breve explicación de cómo implementar el escenario se presenta en la siguiente imagen.
La siguiente imagen muestra que necesitamos crear algunos elementos como Pool de Usuarios Cognito, buckets S3, Métodos de API Gateway, Funciones Lambda, etc. Después de crear todas las entidades en el entorno AWS, necesitamos configurarlas adecuadamente para que puedan trabajar juntas en colaboración.
Arquitectura del Sistema
Arquitectura del Sistema
Es mejor crear todos los elementos en el entorno AWS en orden inverso. Por ejemplo, para usar Lambda con un método API, si la función Lambda se desarrolla primero, cuando se crea el método API Gateway, esta función se puede vincular fácilmente. De manera similar, en el Paso 5 deberíamos crear el bucket S3 web y colocar el archivo callback.html en él, para poder usarlo cuando creemos el Pool de Usuarios Cognito en el Paso 6. Por supuesto, esto no es obligatorio, pero este orden facilitará el desarrollo. Por lo tanto, este enfoque ha sido preferido aquí.

Esquema

Buscaremos respuestas a las siguientes preguntas. Recuerde que necesita tener una cuenta de AWS para implementar todos los pasos de este artículo.
  1. ¿Cómo Crear un Bucket S3 Privado?
  2. ¿Cómo Crear una Política Personalizada para el Permiso de Acceso a Objetos en el Bucket S3 Privado?
  3. ¿Cómo Crear una Función Lambda para Acceder a Objetos en el Bucket S3 Privado?
  4. ¿Cómo Crear un Gateway API para Usar la Función Lambda?
  5. ¿Cómo Crear un Bucket S3 Público para Usar como Carpeta Web?
  6. ¿Cómo Crear y Configurar un Pool de Usuarios Cognito?
  7. ¿Cómo Probar el Escenario?

1. ¿Cómo Crear un Bucket S3 Privado?

S3 es uno de los servicios basados en región en AWS. Los elementos en los buckets S3 se llaman objetos (object). Por lo tanto, en AWS los términos objeto y archivo se pueden usar indistintamente para buckets S3.
Mantenga marcada la casilla "Bloquear Todo el Acceso Público" (Block All Public Access). Aquí se ha creado un bucket S3 privado. Aunque hay muchas opciones de configuración adicionales, lo creamos con valores predeterminados por simplicidad de la solución.
Creación de Bucket S3
Creación de Bucket S3
Cargue algunos objetos en el bucket S3 para probar el acceso privado. Luego, intente acceder a estos objetos con usuarios no autorizados o posibles enlaces de acceso. Aunque conocemos archivos PDF, DOC, XLS, etc., en la terminología de AWS S3 todos se llaman objetos.
Carga de Archivos
Carga de Archivos

2. Creación de una Política para el Permiso de Acceso a Objetos en el Bucket S3 Privado

En AWS, IAM (Identity and Access Management) es la base de todos los servicios. Usuarios, Grupos, Roles y Políticas son los conceptos fundamentales con los que debemos familiarizarnos.
Hay muchos roles integrados (built-in) y cada rol tiene muchas políticas integradas que significan permisos. Estos se llaman "AWS Managed". Sin embargo, también es posible crear roles y políticas "Customer Managed" (Gestionados por el Cliente). Por lo tanto, aquí se ha creado una política personalizada.
  • Cree una política IAM personalizada para recuperar objetos de su bucket S3 privado.
  • Encuentre la lista de políticas existentes en AWS y cree una nueva para ejecutar solo la operación GetObject desde su bucket S3 privado como se muestra a continuación:
Lista de Políticas
Lista de Políticas
Cree una política personalizada como se muestra a continuación. Seleccione S3 como servicio y solo GetObject como acción (action):
Configuración de Política 1
Configuración de Política 1
Seleccione "specific" como recurso (resource) y especifique su bucket S3 privado para que la política tenga las capacidades deseadas:
Configuración de Política 2
Configuración de Política 2
Dé un nombre a su política y créela. Puede dar cualquier nombre pero necesitará recordarlo.
Configuración de Política 3
Configuración de Política 3
El resumen de su política personalizada se verá como sigue. También es posible crear la política usando directamente este contenido JSON:
Política JSON
Política JSON
Definición JSON de la Política:
JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::private-s3-for-interfacing/*"
        }
    ]
}

3. Creación de una Función Lambda para Acceder a Objetos en el Bucket S3 Privado

Aquí se ha utilizado la última versión de NodeJS para la función Lambda. Cree una función Lambda y seleccione NodeJS. Es posible seleccionar cualquier lenguaje soportado como Python, Go, Java, .NET Core, etc. para la función Lambda.
Creación de Lambda
Creación de Lambda
Cuando crea la función Lambda, se muestra un código "hello" de ejemplo. Necesitamos desarrollar nuestro código en su lugar.
Como es visible, el entorno de desarrollo Lambda es similar a un IDE ligero basado en web.
Modificar Código Lambda
Modificar Código Lambda
Reemplace el código existente con el código de ejemplo corto proporcionado. La nueva versión del código será como sigue. Después de modificar el código, presione el botón "Deploy" para usar la función Lambda.
Por simplicidad del escenario, el nombre del bucket se usa estáticamente. El nombre del archivo se envía como parámetro con el nombre fn. Aunque el tipo de contenido (content type) predeterminado se asume como pdf, puede ser cualquier tipo de archivo implementado en el código de la función Lambda. Como preferiremos usar la característica de proxy de la función Lambda en la conexión de API Gateway, el encabezado de respuesta (response header) contiene algunos datos adicionales requeridos.
Código Lambda NodeJS (retorno como Blob):
JavaScript
// El código de la función Lambda se verá así
// Este código devolverá la respuesta como contenido blob
// Para descargar el archivo se puede usar Callback-to-Download-Blob.html en los adjuntos

const AWS = require('aws-sdk');
const S3= new AWS.S3();
exports.handler = async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
  let fileExt;
    
  try {
    bucketName = 'private-s3-for-interfacing';
    fileName = event["queryStringParameters"]['fn']
    contentType = 'application/pdf';
    fileExt = 'pdf';
    
    //------------
    fileExt = fileName.split('.').pop();
    
    switch (fileExt) {
        case 'pdf': contentType = 'application/pdf'; break;        
        case 'png': contentType = 'image/png'; break;
        case 'gif': contentType = 'image/gif'; break;
        case 'jpeg': case 'jpg': contentType = 'image/jpeg'; break;
        case 'svg': contentType = 'image/svg+xml'; break;
        case 'docx': contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; break;
        case 'xlsx': contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; break;
        case 'pptx': contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; break;
        case 'doc': contentType = 'application/msword'; break;
        case 'xls': contentType = 'application/vnd.ms-excel'; break;
        case 'csv': contentType = 'text/csv'; break;
        case 'ppt': contentType = 'application/vnd.ms-powerpoint'; break;
        case 'rtf': contentType = 'application/rtf'; break;
        case 'zip': contentType = 'application/zip'; break;
        case 'rar': contentType = 'application/vnd.rar'; break;
        case '7z': contentType = 'application/x-7z-compressed'; break;
        default: ;
    }
    
    //------------
    const data = await S3.getObject({Bucket: bucketName, Key: fileName}).promise();
    
    return {
       headers: {
          'Content-Type': contentType,
          'Content-Disposition': 'attachment; filename=' + fileName, // Clave del éxito
          'Content-Encoding': 'base64',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 
          'Access-Control-Allow-Methods': 'GET,OPTIONS'
      },
      body: data.Body.toString('base64'),
      isBase64Encoded: true,
      statusCode: 200
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 400,
      body: err.message || JSON.stringify(err.message) + ' - fileName: '+ fileName + ' - bucketName: ' + bucketName
    }
  }
}
También es posible usar código Python en la función Lambda como se muestra a continuación:
Python
# El código siguiente puede ser desarrollado como el ejemplo NodeJS arriba
    
import base64
import boto3
import json
import random

s3 = boto3.client('s3')

def lambda_handler(event, context):
    try:
        fileName = event['queryStringParameters']['fn']
        bucketName = 'private-s3-for-interfacing'        
        contentType = 'application/pdf'
        
        response = s3.get_object(
            Bucket=bucketName,
            Key=fileName,
        )
        
        file = response['Body'].read()
        
        return {
            'statusCode': 200,
            'headers': {  
                         'Content-Type': contentType,                            
                         'Content-Disposition': 'attachment; filename='+ fileName,
                         'Content-Encoding': 'base64'
                         # Se pueden agregar códigos relacionados con CORS aquí si es necesario
                        },
            'body': base64.b64encode(file).decode('utf-8'),           
            'isBase64Encoded': True
        }
    except:
        return {
            'headers': { 'Content-type': 'text/html' },
            'statusCode': 200,
            'body': '¡Ocurrió un error en Lambda!' 
        }
Otro método podría ser crear una presigned URL con Lambda:
JavaScript
// Este método proporcionará una presigned url
// Para usar el enlace presigned URL se puede usar el archivo Callback-for-preSignedUrl.html

var AWS = require('aws-sdk');
var S3 = new AWS.S3({
  signatureVersion: 'v4',
});

exports.handler = async (event, context) => {
    
  let fileName;
  let bucketName;
  let contentType;
    
  bucketName = 'private-s3-for-interfacing';
  fileName = event["queryStringParameters"]['fn'];
  contentType = 'application/json';
    
  const presignedUrl = S3.getSignedUrl('getObject', {
    Bucket: bucketName,
    Key: fileName,
    Expires: 300 // segundos
  });

  let responseBody = {'presignedUrl': presignedUrl};
  
  return {
       headers: {
          'Content-Type': contentType,
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 
          'Access-Control-Allow-Methods': 'GET,OPTIONS'
      },
      body: JSON.stringify(responseBody), 
      statusCode: 200
    }    
};
Cuando se crea la función Lambda, se crea un rol junto con ella. Sin embargo, este rol no tiene permiso para acceder a los objetos en su bucket S3 privado. Ahora necesitamos adjuntar la política "Customer Managed" que creamos en los pasos anteriores a este rol creado con la función Lambda.
Después de crear la función Lambda, podemos encontrar el rol creado automáticamente como se muestra a continuación:
Encontrar el Rol de Lambda
Encontrar el Rol de Lambda
Adjunte la política personalizada que creó en el paso anterior a este rol; así la función Lambda tendrá acceso limitado GetObject a su bucket S3.
Adjuntar Política
Adjuntar Política
Esto es todo lo que se necesita para que Lambda acceda a su bucket S3. Ahora es el momento de crear un método AWS Gateway para usar nuestra función Lambda.

4. Creación de un Gateway API para Usar la Función Lambda

Cree un AWS Gateway REST API como se muestra a continuación. Como es visible, hay muchas opciones pero nosotros creamos una API "REST" como "New API". Dé un nombre a su API Gateway.
Creación REST API
Creación REST API
Hay algunos pasos para crear y hacer funcionar la AWS GW API:
  • Crear API
  • Crear Resource
  • Crear Method
  • Desplegar (Deploy) la API
Cree un Resource para su REST API como se muestra a continuación:
Creación de Resource Paso 1
Creación de Resource Paso 1
El recurso (resource) creado aquí se utilizará posteriormente en la URL de la API.
Creación de Resource Paso 2
Creación de Resource Paso 2
Cree el método GET para el recurso que ha creado:
Creación de Método GET
Creación de Método GET
Aquí se puede crear cualquier método HTTP como GET, POST, PUT, DELETE, etc. Para nuestras necesidades solo creamos GET. No olvide vincular la función Lambda que creamos en pasos anteriores con este método.
Lambda Proxy Integration está seleccionado aquí. Este enfoque nos permite manejar todo el contenido relacionado con la respuesta en la Función Lambda.
Integración Lambda Proxy
Integración Lambda Proxy
Después de crear el método GET, el flujo entre el Método API Gateway y la función Lambda se verá como sigue:
Vista del Flujo
Vista del Flujo
Habilite CORS para el Gateway API como se muestra a continuación. Las opciones Default 4xx y Default 5xx pueden seleccionarse; así incluso los errores pueden devolverse sin problemas.
Habilitar CORS
Habilitar CORS
Después de crear y configurar todo relacionado con el método AWS Gateway, es hora de desplegar (deploy) la API. La API se despliega en un stage como se muestra. También este nombre del stage se utilizará en la URL general de la API.
Despliegue de API
Despliegue de API
Después del despliegue, la URL se verá como sigue. Ahora es posible usar este enlace desde cualquier aplicación.
URL de Despliegue
URL de Despliegue
Para limitar el acceso al API gateway, debemos definir un Authorizer (Autorizador). Podemos definir un Autorizador Cognito como se muestra a continuación.
Como es visible en la siguiente imagen, Authorization es el token JWT que debe agregarse a la parte del encabezado de la solicitud para usar el método API autorizado.
Cuando la UI Hosted de Cognito se envía con un usuario/contraseña Cognito, Cognito redirigirá al usuario hacia la URL de callback transfiriendo id_token y datos state adicionales.
Note que el token que necesitamos agregar al encabezado se llama "Authorization" bajo Token Source.
Definición de Cognito Authorizer
Definición de Cognito Authorizer
Después de que el Autorizador basado en Cognito ha sido definido, puede usarse como sigue:
Uso del Authorizer
Uso del Authorizer
Por otro lado, si no desea definir un Authorizer para el API Gateway, puede limitar el acceso a la URL de la API con "Resource Policy" (Política de Recursos) como se muestra a continuación.
Si la Resource Policy se modifica/agrega, la API debe volver a desplegarse. La IP mostrada como xxx.xxx.xxx.xxx puede ser la IP del servidor. Cuando alguien intenta acceder a la URL desde una IP diferente, se mostrará el siguiente mensaje:
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-2:********8165:... with an explicit deny"}
Configuración de Resource Policy
Configuración de Resource Policy
El código JSON de la Resource Policy será como sigue:
JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "*",
            "Condition": {
                "NotIpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx"
                }
            }
        }
    ]
}

5. Bucket S3 Público para Usar como Carpeta Web

Para la solución necesitamos dos buckets S3 (bucket). El primero fue creado en las secciones anteriores. El segundo se está creando ahora y se usará como carpeta web. El primero se usó como bucket privado para almacenar todos los archivos.
Estructura de Dos Buckets S3
Estructura de Dos Buckets S3
Cree un bucket S3 público como carpeta web. Este bucket contiene un archivo callback.html, por lo que puede usarse como dirección de callback de Cognito.
Creación de Bucket Web
Creación de Bucket Web
El bucket S3 para la web debe ser público (public). Por lo tanto, se puede aplicar la siguiente política:
JSON
// El JSON de la política se verá así

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::web-s3-for-interfacing/*"
        }
    ]
}

Descargar Archivos Fuente

Puede descargar Callback.html y otros archivos fuente desde los siguientes enlaces:

6. Creación y Configuración del Pool de Usuarios Cognito

  • Dirección de callback: https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html
  • OAuth 2.0 Flows: seleccione la opción "implicit grant".
  • OAuth 2.0 Scopes: email, openid, profile.
Examine el enlace de la UI hosted a continuación. Para enviar parámetros a la página de inicio de sesión Cognito hosted, agregue un parámetro URL "state" adicional. El parámetro "state" se pasará al archivo Callback.html.
El enlace de la UI Cognito Hosted contiene muchos parámetros URL como se muestra a continuación:
https://test-for-user-pool-for-s3.auth.eu-west-2.amazoncognito.com/login?client_id=7uuggclp7269oguth08mi2ee04&response_type=token&scope=openid+profile+email&redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html&state=fn=testFile.pdf
Campos:
  • client_id=7uuggclp7269oguth08mi2ee04
  • response_type=token
  • scope=openid+profile+email
  • redirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.html
  • state=fn=testFile.pdf
state es un parámetro URL personalizado. Puede enviarse a la página UI Hosted y devolverse a la página Callback.html.
Debe crearse una app client como se muestra a continuación:
Creación de App Client
Creación de App Client
Las configuraciones de la App client pueden confirmarse como se muestra a continuación:
Configuraciones de App Client
Configuraciones de App Client
Debe configurarse un nombre de dominio (domain name) para poder usarlo como URL para la UI Hosted.
Configuración de Dominio
Configuración de Dominio

7. ¿Cómo Probar el Escenario?

Veamos cómo probar la API que permite acceso limitado utilizando el Pool de Usuarios Cognito.
Cualquier usuario final puede hacer clic en un enlace para iniciar este proceso. Supongamos que tenemos una página web que aloja el siguiente contenido HTML. Como es visible, para cada archivo el enlace es la URL de la UI hosted de Cognito.
El archivo LinkToS3Files.html puede usarse para probar el escenario.

Descargar Archivos de Prueba


Conclusión

Espero que este artículo haya sido útil para quienes recién comienzan con el entorno de nube AWS.

Servicios de Computación en la Nube

Ofrecemos servicios de diseño de infraestructura, migración, gestión y optimización en plataformas AWS, Azure y Google Cloud.

Explorar Nuestro Servicio

Contáctenos

Póngase en contacto con nuestro equipo para obtener información detallada sobre nuestras soluciones de AWS y computación en la nube.

Contacto