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

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.

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.
- ¿Cómo Crear un Bucket S3 Privado?
- ¿Cómo Crear una Política Personalizada para el Permiso de Acceso a Objetos en el Bucket S3 Privado?
- ¿Cómo Crear una Función Lambda para Acceder a Objetos en el Bucket S3 Privado?
- ¿Cómo Crear un Gateway API para Usar la Función Lambda?
- ¿Cómo Crear un Bucket S3 Público para Usar como Carpeta Web?
- ¿Cómo Crear y Configurar un Pool de Usuarios Cognito?
- ¿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.

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.

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
GetObjectdesde su bucket S3 privado como se muestra a continuación:

Cree una política personalizada como se muestra a continuación. Seleccione S3 como servicio y solo
GetObject como acción (action):
Seleccione "specific" como recurso (resource) y especifique su bucket S3 privado para que la política tenga las capacidades deseadas:

Dé un nombre a su política y créela. Puede dar cualquier nombre pero necesitará recordarlo.

El resumen de su política personalizada se verá como sigue. También es posible crear la política usando directamente este contenido 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.

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.

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:

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.
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.

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:
El recurso (resource) creado aquí se utilizará posteriormente en la URL de la API.

Cree el método
GET para el recurso que ha creado:
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.

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:
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.

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.

Después del despliegue, la URL se verá como sigue. Ahora es posible usar este enlace desde cualquier aplicación.

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.

Después de que el Autorizador basado en Cognito ha sido definido, puede usarse como sigue:

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"}
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.

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.
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.pdfCampos:
client_id=7uuggclp7269oguth08mi2ee04response_type=tokenscope=openid+profile+emailredirect_uri=https://web-s3-for-interfacing.s3.eu-west-2.amazonaws.com/Callback.htmlstate=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:

Las configuraciones de la App client pueden confirmarse como se muestra a continuación:

Debe configurarse un nombre de dominio (domain name) para poder usarlo como URL para la UI Hosted.

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 ServicioContá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