Buscando información sobre como proteger una instancia de Cloudfront para permitir acceso solo a usuarios registrados y validados por Cognito, encontré un ejemplo de 2018 de AWS el cual permite esta opción:
Authorization@Edge – How to Use Lambda@Edge and JSON Web Tokens to Enhance Web Application Security
El ejemplo incluye un machote de CloudFormation para crear la solución con un solo click, pero mi intención era aregar dicha funcionalidad a un proyecto actualmente en producción, así que me dí a la tarea de revisar el código ya viejo, para poder implementarlo en 2022.
A grandes rasgos, el ejemplo muestra como una función Lambda es utilizada para validar un JSON Web Token (jwt) de Cognito que es enviado cada ve que hacemos la petición de un archivo por medio de Cloudfront.
👩💻 → Cloudfront → Lambda@Edge → S3
Para iniciar hay que crear una función. Es importante utilizar la arquitectura x86_64
ya que Lambda@Edge no permite ejecutar en arm64
(por el momento).

Luego de creada la función, hay que modificar el rol para que pueda ser ejecutada por Lambda@Edge.

Dentro de Trust relationships hay que agregar edgelambda.amazonaws.com
.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["edgelambda.amazonaws.com", "lambda.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}

Ahora localmente creamos el proyecto con las dependencias y archivos necesarios para la función.
npm init -y
npm install jsonwebtoken jwk-to-pem -S
touch index.js
El cual nos va a generar un archivo package.json
que no es necesario pero funciona como referencia o para futuras actualizaciones de estas dependencias.
package.json
{
"dependencies": {
"jsonwebtoken": "^8.5.1",
"jwk-to-pem": "^2.0.5"
}
}
Y dentro del archivo index.js
copiamos el siguiente código, que es mi versión actualizada del código
de ejemplo de AWS. Solo debemos modificar tres constantes: REGION
, USERPOOLID
y JWKS
con nuestra información donde tenemos la instancia de Cognito.
Los JWKS
los podemos encontrar en un archivo json
que debemos buscar en una url específica a la instancia de Cognito que estamos utilizando
https://cognito-idp.[AWSRegion].amazonaws.com/[UserPoolId]/.well-known/jwks.json
Y agregamos el contenido como una cadena de caracteres (string) en la constante JWKS
.
index.js
const jwt = require("jsonwebtoken");
const jwkToPem = require("jwk-to-pem");
const REGION = "us-east-1";
const USERPOOLID = "us-east-1_XXXXXXXXX";
// https://cognito-idp.[AWSRegion].amazonaws.com/[UserPoolId]/.well-known/jwks.json
const JWKS = "";
const iss = `https://cognito-idp.${REGION}.amazonaws.com/${USERPOOLID}`;
const keys = JSON.parse(JWKS).keys;
const pems = keys.reduce((o, key) => {
// Convert each key to PEM
const key_id = key.kid;
const modulus = key.n;
const exponent = key.e;
const key_type = key.kty;
const jwk = { kty: key_type, n: modulus, e: exponent };
const pem = jwkToPem(jwk);
return { ...o, [key_id]: pem };
}, {});
// Cloudfront Response
const response401 = {
status: 401,
body: "Unauthorized",
};
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers || {};
// CORS Preflight
if (`${request.method}`.toUpperCase() === "OPTIONS") {
return request;
}
// Fail if no authorization header found
if (!headers.authorization) {
console.log("No authorization header");
return response401;
}
// Strip out "Bearer " to extract JWT token only
const jwtToken = headers.authorization[0].value.slice(7);
console.log(`jwtToken=${jwtToken}`);
// Fail if the token is not jwt
const decodedJwt = jwt.decode(jwtToken, { complete: true });
if (!decodedJwt) {
console.log("Not a valid JWT token");
return response401;
}
// Fail if token is not from your UserPool
if (decodedJwt.payload.iss !== iss) {
console.log("Invalid issuer");
return response401;
}
// Reject the jwt if it's not an "Access Token"
if (decodedJwt.payload.token_use !== "access") {
console.log("Not an access token");
return response401;
}
// Get the kid from the token and retrieve corresponding PEM
const kid = decodedJwt.header.kid;
const pem = pems[kid];
if (!pem) {
console.log("Invalid access token");
return response401;
}
try {
// Verify the signature of the JWT token to ensure
// it's really coming from your User Pool
jwt.verify(jwtToken, pem, { issuer: iss });
// Valid token.
console.log("Successful verification");
// Remove authorization header
delete request.headers.authorization;
// CloudFront can proceed to fetch the content from origin
return request;
} catch (err) {
// Invalid token.
console.log("Token failed verification");
return response401;
}
};
Después hay que crear un archivo .zip
para subir directamente a la función, ya que no se pueden agregar Layers a funciones que son ejecutadas en Edge. Para esto solo necesitamos el archivo index.js
y el directorio node_modules/
zip -r lambda.zip index.js node_modules
Una vez creado el archivo .zip
, lo subimos a la función.


Cuando tenemos el código listo con nuestra información y hacemos un “Deploy”, es tiempo de crear una “Version” para que pueda ser publicada en Edge y utilizada por Cloudfront.

Una vez creada la “Version” copiamos el ARN de la función.

Dentro del bucket de S3 hay que permitir el acceso con la siguiente configuración CORS:
[
{
"AllowedOrigins": ["*"],
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"ExposeHeaders": []
}
]
En el caso de AllowedOrigins
se puede ser tan específico se requiera, para solo permitir el acceso a los archivos a ciertos dominios.
En la distribución de Cloudfront, modificamos el “Behavior” y asociamos en “Viewer Request” una función de “Lambda@Edge” con el ARN de la versión de la función que creamos.

AWS Amplify tiene una opción dentro de la librería de autenticación Auth
para obtener la sesión del usuario por medio de Auth.currentAuthenticatedUser()
o solo la sesión activa Auth.currentSession()
, en este caso vamos a obtener toda la información del usuario para el ejemplo.
En caso de React, es posible obtener la información del usuario de la siguiente manera:
const [user, setUser] = useState(null);
useEffect(() => {
Auth.currentAuthenticatedUser()
.then((user) => setUser(user))
.catch((err) => console.log(err));
}, []);
Ahora, cada vez que necesitamos acceder a la información protegida, es necesario agregar Authorization
dentro de los encabezados (headers) con el accessToken
del usuario autenticado por Cognito, como por ejemplo, utilizando fetch
:
const token = user?.signInUserSession?.accessToken?.jwtToken ?? "";
const res = await fetch("https://example.com/private/top-secret.json", {
headers: {
Authorization: `Bearer ${token}`,
},
});
En caso de utilizar hls.js la configuración es la siguiente:
const token = user?.signInUserSession?.accessToken?.jwtToken ?? "";
const config = {
xhrSetup: (xhr, url) => {
xhr.open("GET", url, true);
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
},
};
Si el usuario intenta entrar directamente al archivo, lo único que va a ver es un error 401
que indica que no esta autorizado para acceder.