AWS Lambda + API Gateway + Cognito. Parte III.

En esta nueva entrada, vamos a ver cómo podemos configurar Cognito para autenticarnos y haremos un pequeño servicio en Java para realizar la autenticación, el cual podemos desplegar en AWS Lambda + API Gateway (tal y como hemos visto en las entradas anteriores) para poder realizar la autenticación desde cualquier lugar.

Lo primero que vamos a hacer es entrar en nuestra consola de AWS y buscar el servicio de Cognito. Una vez lo hemos seleccionado, nos aparecerá una pantalla donde podremos Administrar Grupos de Usuarios, el cual nos va a permitir crear grupos de usuarios los cuales podrán iniciar sesión para acceder a alguno de nuestros servicios.

Al acceder por primera vez, puesto que no tenemos ningún grupo de usuarios creados, nos pedirá que creemos uno nuevo.

A continuación, debemos dar un nombre descriptivo a nuestro nuevo grupo de usuarios.

Lo siguiente que podemos hacer es ir paso por paso configurando nuestro grupo de usuarios, atributos, políticas de seguridad, autenticación multifactor (MFA), etc. Nosotros por ahora, vamos a dejar los valores por defecto, así que podemos darle directamente a Revisar los valores predeterminados, donde se nos mostrará un resumen de todas las opciones configuradas.

Una vez creemos nuestro grupo, veremos que nos ha creado un ID de Grupo, el cual necesitaremos más adelante para que nuestra aplicación se conecte a Cognito y pueda realizar la autenticación.

vamos a irnos a la opción de Clientes de aplicación. Esto nos permitirá tener un ID único y una clave secreta (opcional) que nos servirá para que nuestra aplicación pueda conectarse y tener acceso a este grupo.

Añadimos un nuevo cliente de aplicación, y seleccionamos las casillas que se muestran a continuación.

Por defecto, el token que nos generará la aplicación de cliente tiene un vencimiento de 30 días. No es recomendable tener una duración de token válida tan alta, ya que si por lo que fuera nos robaran el token, podría utilizarlo para suplantar nuestra identidad. Pero aquí ya va a depender de las necesidades de cada aplicación. Puesto que esto es un simple ejemplo, lo podemos dejar como está.

Una vez lo generamos, veremos que nos ha creado un ID de cliente de aplicación. Este ID, junto con el ID del grupo que hemos visto antes, nos servirá para que nuestra aplicación se pueda conectar a Cognito y realizar la autenticación.

Ahora que ya tenemos nuestro grupo creado, vamos a añadir un nuevo usuario al grupo.

Pinchamos sobre Crear un usuario y le indicaremos un nombre de usuario y una contraseña temporal. Si lo deseamos, podemos indicar un nº de teléfono y un email, los cuales podemos marcar como verificados o requerir que sea el usuario quien los verifique. Incluso podemos enviar una invitación al nuevo usuario. (Nota: debemos indicar al menos el correo, ya que si no, nos dará después un error a la hora de autenticarnos)

Finalmente, cuando creemos nuestro usuario, veremos que está habilitado y que se requiere un cambio de contraseña.

Aquí podemos crear tantos usuarios como necesitemos que tengan acceso a la autenticación. Sin embargo, antes de continuar viendo el código para la autenticación, vamos a necesitar las credenciales de un usuario IAM con privilegios para invocar a Cognito.

Lo ideal es crear un usuario que únicamente tenga acceso a este servicio, por lo que dentro de la consola AWS nos iremos al servicio IAM para crear un nuevo usuario.

Nuestro nuevo usuario IAM lo crearemos con acceso programático, ya que solo queremos que acceda a través de la API de AWS y no a través de la consola.

El siguiente paso es darle los permisos necesarios para poder acceder a Cognito. Para ello, le podemos dar a Asociar directamente las políticas existentes y buscar por la palabra cognito para ver las distintas políticas.

Vamos a seleccionar la política de AmazonCognitoPowerUser, ya que como vamos a necesitar cambiar la contraseña del usuario, necesitaremos de esos permisos. Si no fuera así, o si ya la hemos cambiado y sabemos que no se va a necesitar volver a cambiarla, podemos coger una más restrictiva, como la de AmazonESCognitoAcces, que nos permite la autenticación, pero no otras acciones. También podríamos personalizar las políticas y elegir únicamente las que necesitemos, pero por simplicidad lo vamos a hacer así ahora.

El resto de pasos los podemos dejar por defecto y seguir adelante hasta crear el usuario.

Una vez creado, podremos descargarnos las credenciales de este usuario, las cuales necesitaremos en nuestro código para poder acceder a Cognito y que los usuarios se autentiquen.

Bien, ahora que ya tenemos todo lo necesario, vamos a ver cómo implementar en Java nuestro servicio para autenticarnos.

Si creamos un proyecto maven, debemos añadir las siguientes dependecias:

<dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>1.2.1</version>
</dependency>
<dependency>
    	<groupId>com.amazonaws</groupId>
	<artifactId>aws-java-sdk-cognitoidp</artifactId>
  	<version>1.11.269</version>  
</dependency>
<dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.6</version>
</dependency>

Ahora, nos vamos a crear una clase CognitoAuth, la cual se encargará de realizar la autenticación con AWS Cognito.

Lo primero será crear un método para crear un objeto AWSCognitoIdentityProvider el cual usará las credenciales del usuario IAM con permisos AmazonCognitoPowerUser que habíamos creado en el paso anterior. Debemos indicarle el AWS_ACCESS_KEY y el AWS_SECRET_KEY que nos habíamos descargado antes. Además, indicaremos la región donde hemos creado nuestro grupo de usuarios, que en este caso sería el enumerable US_EAST_1 de la clase com.amazonaws.regions.Regions.

private AWSCognitoIdentityProvider cognitoClient;

private AWSCognitoIdentityProvider getCognitoClient(){
	if(cognitoClient == null) {
		cognitoClient = AWSCognitoIdentityProviderClient.builder()
				.withCredentials(new AWSStaticCredentialsProvider(
                                 new BasicAWSCredentials(AWS_ACCESS_KEY,
				AWS_SECRET_KEY))).withRegion(REGION).build();
	}
	return cognitoClient;
}

Lo siguiente, una función que nos sirva para autenticar a los usuario de cognito.

public String login(String username,String password){
		String idToken = null;
		Map<String,String> authParams = new HashMap<String,String>();
		authParams.put("USERNAME", username);
		authParams.put("PASSWORD", password);
		AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest()
				.withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH)
				.withAuthParameters(authParams)
				.withClientId(COGNITO_CLIENT_ID)
				.withUserPoolId(COGNITO_POOL_ID);
		AdminInitiateAuthResult authResponse = getCognitoClient().adminInitiateAuth(authRequest);
		if(StringUtils.isNullOrEmpty(authResponse.getChallengeName())){
			idToken = authResponse.getAuthenticationResult().getIdToken();
		}else {
			idToken = changePassword(username,password,password,authResponse.getSession());
		}
		return idToken;
	}

Y puesto que nuestro usuario requería un cambio de password, necesitaremos también un método para realizar este cambio.

public String changePassword(String username,String password,String resetPassword,String session){
		String idToken = null;
		Map<String,String> challengeResponses = new HashMap<String,String>();
		challengeResponses.put("USERNAME", username);
		challengeResponses.put("PASSWORD", password);
		challengeResponses.put("NEW_PASSWORD", password);
		AdminRespondToAuthChallengeRequest finalRequest = new AdminRespondToAuthChallengeRequest()
				.withChallengeName(ChallengeNameType.NEW_PASSWORD_REQUIRED)
				.withChallengeResponses(challengeResponses)
				.withClientId(COGNITO_CLIENT_ID)
				.withUserPoolId(COGNITO_POOL_ID)
				.withSession(session);
		AdminRespondToAuthChallengeResult challengeResponse = getCognitoClient().adminRespondToAuthChallenge(finalRequest);
		if(StringUtils.isNullOrEmpty(challengeResponse.getChallengeName())){
			idToken = challengeResponse.getAuthenticationResult().getIdToken();
		}
		return idToken;
	}

Desde la función login estamos llamando a esta función changePassword en caso de que se requiera, aunque como se ve, hemos utilizado por simplicidad el mismo password, por lo que en realidad no se estaría cambiando. Podemos modificar esta función si queremos para que devuelva algún error y entonces que se requiera cambiar el password llamando a la función changePassword pasando una nueva contraseña.

Ahora ya solo nos quedaría probar nuestra función. Podemos crear una clase Main para testear nuestro código ejecutando este simple código, indicando por supuesto el usuario y el password que hayamos creado en el grupo de usuarios de Cognito.

public class Main {

	public static void main(String[] args) {
		CognitoAuth cognito = new CognitoAuth();
		
		String token = cognito.login("samir", "MyPassword@1234");
		
		System.out.println(token);
	}

}

Si todo va correctamente, debería imprimirnos un token que será el que usaremos después para tener la autorización necesaria para ejecutar nuestra función Lambda que creamos en nuestro post anterior.

Ahora bien, si queremos desplegar este código en AWS Lambda para ejecutarlo en AWS, necesitamos crear una nueva función, la cual nos va a permitir capturar las llamadas, recuperar el usuario y contraseña enviados en la petición y realizar la llamada a nuestro cognito.login.

public class Handler implements RequestHandler<Map<String,String>, String>{
  Gson gson = new GsonBuilder().setPrettyPrinting().create();
  @Override
  public String handleRequest(Map<String,String> event, Context context)
  {
    LambdaLogger logger = context.getLogger();
    String response = new String("200 OK");
    // log execution details
    logger.log("ENVIRONMENT VARIABLES: " + gson.toJson(System.getenv()));
    logger.log("CONTEXT: " + gson.toJson(context));
    // process event
    
    logger.log("EVENT: " + gson.toJson(event));
    logger.log("EVENT TYPE: " + event.getClass().toString());
    
    CognitoAuth cognito = new CognitoAuth();
	
    return cognito.login(event.get("login"), event.get("password"));
	
  }
}

Aunque solo son necesarias las dos últimas líneas para hacerlo lo más simple posible, podríamos hacer que devolviera un json con una response válida o con errores, con el token dentro de una key, etc. Dejo también los logger que nos permitirán ver en nuestra ejecución en Lambda los valores del context donde se ejecuta la función y en event los parámetros de la llamada. Es de ahí de donde recuperaremos el login y el password.

El código se puede encontrar en https://github.com/secdevoops/aws-cognito

Si lo que queremos ahora es que nuestro código se ejecute en AWS Lambda, una vez generamos el jar, nos iremos a la consola de AWS y nos iremos al servicio Lambda para crear una nueva función, seleccionando como lenguaje Java 11.

Una vez creada, debemos modificar la configuración básica, ya que como vemos en la siguiente imagen, el controlador (la función que se ejecutará al hacer la llamada) es el handleRequest, pero del paquete example.Hello.

Modificaremos ese valor por el paquete y la clase que hemos creado nosotros, en mi caso es.secdevoops.cognito.Handler.

Y el siguiente paso será cargar nuestro jar para que se pueda ejecutar nuestro código.

Ahora ya podemos testear nuestro código ejecutándose en AWS Lambda. Crearemos, igual que hicimos con la función en Python, un evento de prueba, indicando un login y un password.

Una vez lo tenemos todo, solo tenemos que probar nuestra función y ver que nos devuelve nuestro valioso token.

Ya por último, si queremos, podemos unir nuestra función java con API Gateway para poder autenticarnos desde cualquier sitio (el proceso es el mismo que veíamos en la entrada anterior).

En el siguiente artículo veremos finalmente cómo requerir autorización para ejecutar nuestra función Lambda que creamos en Python en esta primera serie de post y utilizar el token que generamos con el login para poder ejecutarlo sin problemas.

Categorías