Kevin Catucuamba
Posted on November 5, 2023
Al desarrollar servicios mediante Amazon API Gateway, es fundamental considerar cómo garantizar la seguridad de nuestros recursos y prevenir el abuso por parte de ciertos usuarios. En el contexto de Amazon API Gateway, existen varias formas de autorizar las solicitudes.
En este breve tutorial, se presentará un ejemplo práctico de cómo asegurar nuestra API Gateway mediante el uso de tokens. Utilizaremos una función Lambda como autorizador, y los tokens serán generados por el servicio de Amazon Cognito, que cuenta con un conjunto de usuarios.
Objetivos del ejemplo:
- Implementar los recursos esenciales en AWS utilizando AWS SAM (Serverless Application Model), y validar la correcta creación de estos recursos en la AWS Management Console.
- Configurar, a través de una plantilla con especificaciones OpenAPI, todas las medidas necesarias para asegurar una API, garantizando su protección integral.
- Demostrar el proceso de autenticación de un usuario a través de Amazon Cognito y cómo enviar el token correspondiente para habilitar la invocación de una API en AWS de manera segura.
- Realizar pruebas de la funcionalidad de la función Lambda autorizadora implementada, asegurando su correcto funcionamiento.
En la consola de AWS se visualizarán los recursos que se deben crear, por lo que es importante tener un entendimiento básico de las funciones Lambda y las APIs en AWS.
Lambda Authorizer
Una Lambda Authorizer es un componente del servicio Amazon API Gateway que gestiona el acceso a las APIs y los recursos de backend. Opera antes de que las solicitudes de una API en Amazon API Gateway se procesen y determina si un usuario o una solicitud de aplicación tiene el permiso necesario para acceder a los recursos protegidos.
En este caso, se presentará un ejemplo de una Lambda Authorizer basada en tokens:
1.- Un cliente efectúa una solicitud HTTP hacia un servicio a través de Amazon API Gateway.
2.- Amazon API Gateway transmite la información requerida, incluyendo el token, a la función lambda encargada de la autorización. En esta función, se lleva a cabo la validación del token o acciones necesarias para determinar su validez.
Ejemplo de los datos de entrada:
{
"type": "TOKEN",
"methodArn": "arn:aws:execute-api:us-east-1:818802983213:4dxds2wsdf/v1/GET/labs/products",
"authorizationToken": "Bearer eyJraWQi...."
}
3.- Dentro de la lambda se implementa la lógica necesaria para validar el token y retornar un documento de politica que contiene una lista de declaraciones de políticas, indicando si pemite o deniega el acceso.
Ejemplo de politica generada:
{
"principalId": "user/unauthorized",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": [
"arn:aws:execute-api:us-east-1:00XXXXXXXX00:XXXXXXXXX/v1/GET/labs/products"
]
}
]
},
"context": {
"key": "value",
"number": 1,
"bool": true
}
}
4.- Devuelve la política generada a Amazon API Gateway para su evaluación.
5.- En caso de que la política generada permita el acceso al recurso configurado, el acceso al recurso desplegado, que en este caso es una función lambda, es autorizado.
Pool de Usuarios
Un grupo de usuarios (Pool Users) de Amazon Cognito es un directorio de usuarios para la autenticación y autorización de aplicaciones web y móviles. Un pool de usuarios en Amazon Cognito actúa como un repositorio centralizado de usuarios y proporciona funciones de autenticación, autorización y administración de usuarios para sus aplicaciones.
Mayor información en: Grupos de usuarios de Amazon Cognito
Plantilla de SAM para crear un pool de usuarios.
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
SAM template for the Cognito resources.
Parameters:
EnvironmentId:
Type: String
Description: Environment Name
CompanyPrefix:
Type: String
Description: "A name for environment id"
Resources:
SystemsUsersPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName : !Join ['-',[!Ref "EnvironmentId" ,"system-users" ]]
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
UserPoolTags:
ORGANIZATION: !Ref CompanyPrefix
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
AutoVerifiedAttributes:
- email
AliasAttributes:
- email
Schema:
- AttributeDataType: String
Name: email
Required: true
Mutable: true
ClientAppAuth:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref SystemsUsersPool
GenerateSecret: true
ReadAttributes:
- address
- nickname
- birthdate
- phone_number
- email
- phone_number_verified
- email_verified
- picture
- family_name
- preferred_username
- gender
- profile
- given_name
- zoneinfo
- locale
- updated_at
- middle_name
- website
- name
SystemsUsersPool: Este recurso representa un grupo de usuarios en Cognito. Este recurso se utiliza para crear un grupo de usuarios con configuraciones específicas, como políticas de contraseña y atributos permitidos.
ClientAppAuth: Es un recurso que representa una aplicación cliente que se autentica con el grupo de usuarios creado anteriormente. Este recurso define las propiedades de la aplicación cliente, como los atributos que se pueden leer y si se debe generar un secreto para la autenticación.
En el servicio de Amazon Cognito se puede observar los grupos de usuarios creados:
Por defecto, el grupo de usuarios recién creado no incluirá a ningún usuario. Para agregar uno, se debe crear un nombre de usuario, proporcionar una dirección de correo electrónico y establecer una contraseña que cumpla con la política especificada.
Una vez que se ha creado el usuario, este se encontrará en un estado en el que debe cambiar su contraseña. Para confirmar la creación del usuario, se puede utilizar el siguiente comando:
aws cognito-idp admin-set-user-password --user-pool-id us-east-1_7W6X0n6v0 --username userdemo --password kevin??890KZNA --permanent
Para conocer más comandos disponibles visitar: cognito-idp
En este escenario, ya existen usuarios registrados y preparados para autenticarse y generar tokens.
Nota: Para ejecutar este comando, es necesario configurar las credenciales en el archivo "credentials" de AWS.
Configuración en especifiación
Es importante tener en cuenta que es necesario configurar la propiedad "securitySchemes" a nivel de la especificación dentro de los componentes.
Tener en cuenta que se configuran dos partes: "api_key" y "authorizer". El último es el encargado de validar el token dentro de la función lambda autorizadora..
securitySchemes:
api_key:
type: apiKey
in: header
name: x-api-key
description: 'API key or aborts with Unauthorized'
authorizer:
type: apiKey
in: header
name: Authorization
description: 'Token JWT para el consumo del API.'
x-amazon-apigateway-authtype: custom
x-amazon-apigateway-authorizer:
type: token
identitySource: v1
authorizerUri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaAuthorizer}:production/invocations
authorizerResultTtlInSeconds: 3000
identityValidationExpression: ^Bearer [-0-9a-zA-z\.]*$
A nivel de cada endpoint, es esencial garantizar que se utilicen esos esquemas de seguridad.
Una vez que la API se haya desplegado con estas configuraciones, se podrá verificar los cambios en la consola de AWS:
Ejemplo de código
Autenticación de usuario:
Un ejemplo para autenticar a un usuario de nuestro pool es el siguiente código en Node.js:
Command file
import {EnvUtil} from "../utils/env.util";
import {CognitoIdentityProviderClient, InitiateAuthCommand} from "@aws-sdk/client-cognito-identity-provider";
import {AuthLoginResponse, AuthUserCredentials} from "../models/auth-login.model";
import {loggerUtil as log} from "../utils/logger.util";
import crypto from "crypto";
import {HttpStatusCode} from "axios";
export const LogInCommandExecutor = async (authUserCredentials: AuthUserCredentials): Promise<AuthLoginResponse> => {
const [region, clientId, secretClient] = EnvUtil.getObjectEnvVarOrThrow(
['AUTH_AWS_REGION', 'AUTH_CLIENT_ID', 'AUTH_SECRET_CLIENT']);
const {username, password} = authUserCredentials;
const client = new CognitoIdentityProviderClient({region});
const hash = crypto.createHmac('sha256', secretClient).update(`${username}${clientId}`).digest('base64');
const command = new InitiateAuthCommand({
AuthFlow: "USER_PASSWORD_AUTH",
ClientId: clientId,
AuthParameters: {
USERNAME: username,
PASSWORD: password,
SECRET_HASH: hash
}
});
try {
const response = await client.send(command);
if (!response.AuthenticationResult && response.ChallengeName === "NEW_PASSWORD_REQUIRED") {
return {
statusMessage: "It is necessary to change the password",
credentials: null
}
}
return {
statusMessage: "Authentication successful",
credentials: {
idToken: response.AuthenticationResult.IdToken,
accessToken: response.AuthenticationResult.AccessToken,
refreshToken: response.AuthenticationResult.RefreshToken
}
}
} catch (error) {
const errorResponse = {
status: 500,
body: "Hay muchos errores",
headers: {
"Content-Type": "application/json",
}
}
throw new Error(HttpStatusCode.InternalServerError.toString());
} finally {
client.destroy();
}
}
En términos generales, este código realiza la autenticación de un usuario en Cognito utilizando su dirección de correo y contraseña. Utiliza el SDK de Cognito para Node.js y recupera todas las variables de entorno necesarias para llevar a cabo la autenticación:
- región: La región en la que se ha desplegado el grupo de usuarios.
- clientId: Un identificador único para el cliente de integración.
- secretClient: Es una cadena constante que nuestra aplicación debe incluir en todas las solicitudes de API al cliente de la aplicación..
Puedes obtener esta información en la pestaña "App Integration" de Amazon Cognito:
Para obtener más información de las diferentes acciones que se puede realizar, vistiar: API Reference Cognito - Actions
Verificación de token:
Dentro de la función lambda autorizadora, es esencial verificar la validez del token recibido. Si el token es válido, la función debe devolver una política que indique si se permite o deniega el acceso al recurso. La lógica puede ser la siguiente (Node.js):
import {APIGatewayAuthorizerResult, APIGatewayTokenAuthorizerEvent} from "aws-lambda";
import {CognitoJwtVerifier} from "aws-jwt-verify";
const AWS_ACCOUNT = process.env.AUTH_AWS_ACCOUNT_ID;
const AWS_REGION = process.env.AUTH_AWS_REGION;
export class KcUtil {
static ALLOW_TEXT = "Allow";
static DENY_TEXT = "Deny";
static PRINCIPAL_ID = "user|kcatucuamba";
static async validateToken(token: string): Promise<boolean> {
const userPoolId = process.env.AUTH_USER_POOL_ID;
const clientId = process.env.AUTH_CLIENT_ID;
const verifier = CognitoJwtVerifier.create({
userPoolId,
tokenUse: "access",
clientId
});
token = token.replace("Bearer ", "");
try {
const response = await verifier.verify(token as any);
return true;
}catch (e) {
return false;
}
}
static async generatePolicy(event: APIGatewayTokenAuthorizerEvent): Promise<APIGatewayAuthorizerResult> {
const isTokenFailed = !(await KcUtil.validateToken(event.authorizationToken));
if (isTokenFailed) {
return await KcUtil.createPolicy("user/unauthorized", KcUtil.DENY_TEXT, event.methodArn);
}
const resource = "arn:aws:execute-api:" + AWS_REGION + ":" + AWS_ACCOUNT + ":*/*/*/*";
return await KcUtil.createPolicy(KcUtil.PRINCIPAL_ID, KcUtil.ALLOW_TEXT, resource);
}
static async createPolicy(
principalId: string,
effect: string,
resource: string): Promise<APIGatewayAuthorizerResult> {
return {
principalId: principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: [resource]
}
]
},
context: {
key: 'value',
number: 1,
bool: true
}
};
}
}
Se adjunta el archivo package.json que especifica las dependencias utilizadas tanto para la lambda autorizadora como para la generación del token:
"devDependencies": {
"@types/aws-lambda": "^8.10.114",
"@types/axios": "^0.14.0",
"@types/jsonwebtoken": "^9.0.2",
"@types/jwk-to-pem": "^2.0.1",
"@types/node": "^18.15.11",
"aws-lambda": "^1.0.7",
"aws-sdk": "^2.1409.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.3"
},
"dependencies": {
"@aws-sdk/client-cognito-identity-provider": "^3.363.0",
"aws-jwt-verify": "^4.0.0",
"axios": "^1.4.0",
"crypto": "^1.0.1",
"jsonwebtoken": "^9.0.0",
"jwk-to-pem": "^2.0.5",
"winston": "^3.8.2"
}
Verificando funcionalidad
Token:
Generamos el token (accessToken) utilizando otra API de Amazon y funciones lambda en este caso:
1.- Enviando token inválido
En primer lugar, llevamos a cabo una prueba sin enviar un token válido. Dependiendo de la configuración de la API, esta responderá con un mensaje de error en este caso:
Verificamos la traza generada:
Es claro que la validación se lleva a cabo exclusivamente en la función lambda autorizadora, y no se intenta acceder al endpoint en sí, ya que el token ha fallado. Como resultado, se ha generado una política denegada para el recurso que se intentaba acceder a través de API Gateway.
- Enviando token válido
Si enviamos el token previamente generado (accessToken), la API debería realizar una llamada a la función lambda autorizadora. Si la validación es exitosa, la API ejecutará y permitirá el acceso a los recursos configurados, que en este caso también son funciones lambda.
Verificamos la traza:
La sección anaranjada indica que el token ha sido validado con éxito en la función lambda autorizadora, lo que permite el acceso al recurso requerido, en este caso, una lambda que realiza una consulta de productos.
Si se envía nuevamente una solicitud con el mismo token, ocurre algo interesante:
Se observa que no se lleva a cabo la validación en la función lambda autorizadora. Esto se debe a la configuración de una caché de una hora. Es decir, como el token previamente se había verificado y resultó válido, se almacena en la caché durante ese período y no se vuelve a verificar en las siguientes solicitudes. Esta estrategia es beneficiosa en términos de tiempo de respuesta, ya que no sería eficiente realizar la validación en cada petición.
Conclusiones
- La función lambda autorizadora es independiente del lenguaje o la tecnología utilizada, lo que demuestra la flexibilidad y facilidad para implementar estos esquemas de seguridad con las herramientas permitidas por AWS.
- Es esencial tener en cuenta el tiempo de caché de la función lambda autorizadora, el cual debe adaptarse a la duración del token. En este ejemplo, se ha configurado una duración de una hora debido a la vigencia del token.
- La configuración de seguridad implementada aquí es una de las más simples disponibles. Se recomienda consultar la documentación oficial para implementar técnicas de seguridad más avanzadas.
Referencias
- https://aws.amazon.com/blogs/security/use-aws-lambda-authorizers-with-a-third-party-identity-provider-to-secure-amazon-api-gateway-rest-apis/
- https://docs.aws.amazon.com/cognito/latest/developerguide/authentication.html
- https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html
Posted on November 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024