Login en la consola AWS vía Cognito

¿Cómo funciona?

Para empezar la solución requiere de código custom (encuéntralo aquí), lo más importante es entender que vamos a crear credenciales temporales  IAM STS desde Cognito y con ellas generaremos un token de federación que usaremos para crear una URL de login sin password en la consola de AWS.

Un momento, ¿podemos ir paso a paso?

Por supuesto, primero necesitamos describir a los actores en este viaje.
Los servicios de AWS que usaremos son: Cognito UserPool, Cognito IdentityPool y la API de federación.
Los objetos que estos servicios producen respectivamente son tokens JWT, tokens STS y tokens Signing.

Cognito UserPool

Cognito UserPool actúa como un servidor de identidad, lo usaremos para intercambiar inicios de sesión provenientes de nuestro servidor de identidad corporativa (como Google o AzureAD) por tokens JWT (Json Web Token). Dichos tokens también se pueden usar directamente para autenticar y autorizar nuestras aplicaciones implementadas en AWS. En nuestro caso los vamos a volver a cambiar por credenciales temporales de AWS en forma de tokens STS.

Cognito IdentityPool

Cognito IdentityPool crea credenciales temporales STS y las relaciona a roles de IAM que ya existen en nuestra cuenta de AWS. Para obtener un token STS de Cognito IdentityPool, primero debemos crear una federación a un servidor de identidad y luego proporcionarle un token JWT firmado por dicho servidor que intercambiamos por el token STS. Dicho servidor de identidad puede ser Cognito UserPool o cualquier otro, en nuestro caso de uso, es efectivamente Cognito UserPool.

El token de STS generado se comporta exactamente igual que si hubiéramos utilizado la propia API de AWS STS para obtener las credenciales de STS directamente emitiendo una llamada a la API de "assume role", no hay ninguna diferencia.

Federation API

El API de federación proporciona Signing tokens (tokens de firma) que se pueden usar para iniciar sesión directamente en la consola AWS usando una llamada GET. Este es probablemente el servicio menos documentado oficialmente de este caso de uso. La única documentación que encontré al respecto no mencionaba (en el momento de escribir esta publicación) que podemos usar tokens STS producidos por Cognito para intercambiarlos por Signing tokens.

La API de federación proporciona más servicios, pero lo que necesitamos aquí es intercambiar una vez más el token STS generado por un Signing token. Este Signing token nos permitirá construir una URL (también al API de federación) que redirigirá nuestro navegador web directamente a la consola de AWS asumiendo el rol para el que Cognito IdentityPool generó el token de STS.

Vale, ya lo tengo claro, ¿Puedes describir el proceso?

Creamos una configuración estándar de Cognito UserPool, la única particularidad es que declaramos un atributo personalizado llamado "grupos" y lo asignamos a un Azure AD externo federado con SAML. Nuestro proveedor de identidad de origen es un Azure AD de prueba que hemos federado a Cognito UserPool. Hemos asignado los grupos de AzureAD a una claim de SAML denominada "grupos". Esta claim se trasladará al JWT generado por Cognito UserPool y Cognito IdentityPool la utilizará para asignar los grupos de AD a los roles de IAM.

Graphical user interface, text, application, AWS, Cognito

Detalle del mapeo de grupos de AD a roles IAM en Cognito IdentityPool:

Graphical user interface, text, application, AWS, Cognito

Configuración del App Client:

Graphical user interface, text, application, AWS, Cognito

Podemos encontrar directamente un elemento de Cognito UserPool llamado Hosted UI en la configuración del App Client. Al hacer clic en él, nos redirige a cualquiera de los proveedores de identidad federados en Cognito UserPool y asignados al App Client. Recordad que Cognito UserPool es en sí mismo un proveedor de identidad y también puede ser uno de los proveedores permitidos en nuestra App Client. Una vez autenticados en Azure AD, seremos redirigidos a la URL de callback que hemos configurado en nuestra Client App, seremos redirigidos con un parámetro de consulta llamado "code" que contiene el  CodeGrant.

Para recibir el CodeGrant y procesarlo, hemos creado una API HTTP de AWS APIGateway y la configuramos como la URL de callback en la Client App de Cognito UserPool. La API tiene una única integración que es una función de AWS Lambda escrita en Golang que proporciona la funcionalidad descrita a continuación.

Esto significa que podemos usar directamente nuestro navegador para apuntar a la Hosted UI de Cognito UserPool y todo el proceso se llevará a cabo en una sola sesión web, terminando con la sesión iniciada en la consola de AWS en nuestro navegador.

El CodeGrant recibido por nuestra función Lambda a través de la API  de GatewayAPI es solo un string que usaremos para llamar al API de Cognito UserPool para intercambiarla por un token JWT:

codeGrant := string(r.QueryStringParameters["code"])

tokenURL := fmt.Sprintf("https://%s.auth.%s.amazoncognito.com/oauth2/token", a.UserPoolDomain, a.AWSRegion)

v.Set("grant_type", "authorization_code")

v.Set("code", codeGrant)

tokenRequest, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))

tokenResponse, err := HTTPClient.Do(tokenRequest)
type Token struct {

AccessToken string `json:"access_token"`

RefreshToken string `json:"refresh_token"`

IdToken string `json:"id_token"`

TokenType string `json:"token_type"`

ExpiresIn string `json:"expires_in"`

}

var token Token

json.Unmarshal(bodyText, &token)

Una vez que tengamos el token JWT, llamaremos al API de Cognito IdentityPool para obtener una "identity", este es un objeto IdentityPool que debemos proporcionar al siguiente paso:

identity, err := a.IdentityPoolClient.GetId(&identityPool.GetIdInput{

IdentityPoolId: aws.String(a.IdentityPoolID),

Logins: map[string]*string{cognitoURL: &token.IdToken},

})

Con la "identidad" y el token JWT juntos podemos solicitar un token STS. Un token de STS son simplemente unas credenciales temporales de AWS. En este punto, podríamos descargarlas localmente y usarlas, por ejemplo, para autenticar nuestra aplicación de línea de comandos aws o cualquier otro servicio que consuma el API de AWS:

getCredsInput := &identityPool.GetCredentialsForIdentityInput{

Logins: map[string]*string{cognitoURL: &token.IdToken},

IdentityId: identity.IdentityId,

}

stsToken, err := a.IdentityPoolClient.GetCredentialsForIdentity(getCredsInput)

Llamaremos a la API de Federación para recuperar el token de Signin:

request_url := "https://signin.aws.amazon.com/federation"

loginTokenRequest, err := http.NewRequest("GET", request_url, nil)

q := loginTokenRequest.URL.Query()

q.Add("Action", "getSigninToken")

q.Add("SessionDuration", "43200")

q.Add("Session", string(sessionTokenJson))

loginTokenRequest.URL.RawQuery = q.Encode()

loginTokenResponse, err := HTTPClient.Do(loginTokenRequest)

Con el Signing token generamos (sin llamarla) una URL que apunta al API de Federación. El token en si es uno de los argumentos de la URL:

q := loginRequest.URL.Query()

q.Add("Action", "login")

q.Add("Issuer", issuer)

q.Add("Destination", "https://console.aws.amazon.com/")

//Add the Siging Token as a parameter

q.Add("SigninToken", string(token))

loginRequest.URL.RawQuery = q.Encode()

Luego pasamos a GatewayAPI un “HTTP redirect status” con la URL generada en el paso anterior como la URL de redirección. Esto hará que nuestro navegador llame a dicha URL. Se llevará a cabo una última redirección automáticamente que nos llevará directamente la consola de AWS con una sesión ya iniciada como si hubiéramos asumido el rol que Cognito IdentityPool tiene mapeado con nuestro grupo de Azure AD.

Conclusiones

Como habéis visto, no hay magia ni cartón, solo una concatenación de diferentes APIs de autorización de AWS. Nuestra solución ha sido probada en producción; lo estamos usando nosotros mismos. La implementación se despliega usando Terraform, en nuestro sistema en producción hay un poco más de configuración extra que la descrita en este artículo.
Aquí hay un video que explica la solución y el código paso a paso.
El código fuente de la función Lambda descrita está aquí y también la solución completa de Terraform (incluidas las lambdas) aquí.

 

Guia introduccion MuleSoft AnyPoint