Autentication serverless con Cognito
Introducción
En este artículo vamos a ver cómo utilizar Terraform y Go para crear una API serverless usando API Gateway, Lambda y Go. También manejaremos la autenticación con AWS Cognito. El repositorio con los archivos lo podés encontrar aquí.
Terraform
En este ejemplo utilicé Terraform 0.12, y me gustaron bastante los nuevos cambios. Se siente más natural a la hora de describir las configuraciones, como si estuvieras programando. Sin embargo, creo que esta versión tiene más bugs de lo habitual, pero me gusta mucho el nuevo formato de salida para los comandos plan
, apply
, etc. Volviendo al artículo, como hay mucho código, iré actualizando el post gradualmente con más notas y contenido. Es posible que haga otro post explicando otra sección, pero esta versión inicial solo va a mostrar la parte de Cognito, cómo hacerlo funcionar y cómo probarlo.
Cognito
resource "aws_cognito_user_pool" "pool" {
name = "api-skynetng-pw"
username_attributes = ["email"]
# This setting is what actually makes the confirmation code to be sent
auto_verified_attributes = ["email"]
email_configuration {
source_arn = var.email_address_arn
}
}
resource "aws_cognito_user_pool_client" "client" {
name = "client"
user_pool_id = aws_cognito_user_pool.pool.id
explicit_auth_flows = ["ADMIN_NO_SRP_AUTH", "USER_PASSWORD_AUTH"]
}
data "aws_cognito_user_pools" "this" {
name = var.cognito_user_pool_name
depends_on = ["aws_cognito_user_pool.pool"]
}
Como podemos ver, es realmente simple tener un pool de usuarios de Cognito funcionando. La parte más importante aquí es auto_verified_attributes
, ya que eso es lo que hace que Cognito realmente envíe un correo electrónico o un SMS con el código de confirmación. El resto es bastante autoexplicativo: se crea un pool y un cliente. Lo que necesitamos para poder interactuar con nuestro pool es el cliente, por lo que esa parte es de considerable importancia, incluso si la mayoría de las cosas están con valores predeterminados. Como habrás notado, definimos dos explicit_auth_flows
, y eso es para poder interactuar con este pool de usuarios usando usuario y contraseña.
ACM
Ahora, veamos cómo gestionamos la creación del certificado utilizando ACM.
#####################
# SSL custom domain #
#####################
data "aws_acm_certificate" "api" {
domain = var.domain_name
depends_on = [aws_acm_certificate.api]
}
resource "aws_acm_certificate" "api" {
domain_name = var.domain_name
validation_method = "DNS"
}
resource "aws_acm_certificate_validation" "cert" {
certificate_arn = aws_acm_certificate.api.arn
validation_record_fqdns = [aws_route53_record.cert_validation.fqdn]
}
resource "aws_api_gateway_domain_name" "api" {
domain_name = var.domain_name
certificate_arn = aws_acm_certificate.api.arn
}
Aquí, básicamente, creamos el certificado utilizando aws_acm_certificate
y lo validamos automáticamente usando el método DNS
y el recurso aws_acm_certificate_validation
. Los otros recursos en el archivo están ahí porque están algo asociados, pero no necesariamente tienen que estar presentes.
Route53
En esta sección simplemente creamos un registro alias para el API Gateway y el registro de validación.
data "aws_route53_zone" "zone" {
name = substr(var.domain_name, 4, -1)
}
resource "aws_route53_record" "api" {
name = var.domain_name
type = "A"
zone_id = data.aws_route53_zone.zone.zone_id
alias {
evaluate_target_health = true
name = aws_api_gateway_domain_name.api.cloudfront_domain_name
zone_id = aws_api_gateway_domain_name.api.cloudfront_zone_id
}
}
resource "aws_route53_record" "cert_validation" {
name = aws_acm_certificate.api.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.api.domain_validation_options.0.resource_record_type
zone_id = data.aws_route53_zone.zone.id
records = [aws_acm_certificate.api.domain_validation_options.0.resource_record_value]
ttl = 60
}
API Gateway
Aunque este archivo puede parecer relativamente simple, el API Gateway tiene muchas funcionalidades y puede volverse realmente complejo rápidamente. Básicamente, lo que estamos haciendo aquí es crear una API con un recurso que acepta todos los tipos de métodos y los envía tal cual a nuestra función Lambda.
# https://www.terraform.io/docs/providers/aws/guides/serverless-with-aws-lambda-and-api-gateway.html
resource "aws_api_gateway_rest_api" "lambda-api" {
name = replace(var.domain_name, ".", "-")
}
resource "aws_api_gateway_resource" "proxy" {
rest_api_id = aws_api_gateway_rest_api.lambda-api.id
parent_id = aws_api_gateway_rest_api.lambda-api.root_resource_id
path_part = "{proxy+}"
}
resource "aws_api_gateway_method" "proxy" {
rest_api_id = aws_api_gateway_rest_api.lambda-api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = "ANY"
authorization = "NONE"
}
resource "aws_api_gateway_base_path_mapping" "api" {
api_id = aws_api_gateway_rest_api.lambda-api.id
stage_name = aws_api_gateway_deployment.lambda-api.stage_name
domain_name = aws_api_gateway_domain_name.api.domain_name
}
resource "aws_api_gateway_integration" "lambda-api" {
rest_api_id = aws_api_gateway_rest_api.lambda-api.id
resource_id = aws_api_gateway_method.proxy.resource_id
http_method = aws_api_gateway_method.proxy.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api.invoke_arn
}
resource "aws_api_gateway_deployment" "lambda-api" {
depends_on = [aws_api_gateway_integration.lambda-api]
rest_api_id = aws_api_gateway_rest_api.lambda-api.id
stage_name = "prod"
}
resource "aws_lambda_permission" "lambda_permission" {
action = "lambda:InvokeFunction"
function_name = replace(var.domain_name, ".", "-")
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.lambda-api.execution_arn}/*/*/*"
depends_on = [aws_lambda_function.api]
}
Lambda
Este archivo contiene la definición de la función Lambda, la política y los roles necesarios. Básicamente, la política permite registrar en CloudWatch y realizar inspecciones con X-Ray. Luego, el grupo de logs se encarga de almacenar los registros, configurando el período de retención, que por defecto es de 7 días.
resource "aws_lambda_function" "api" {
filename = "../src/main.zip"
function_name = replace(var.domain_name, ".", "-")
role = aws_iam_role.iam_for_lambda.arn
handler = "main"
source_code_hash = filebase64sha256("../src/main.zip")
runtime = "go1.x"
environment {
variables = local.environment_variables
}
}
resource "aws_iam_role" "iam_for_lambda" {
name = "${replace(var.domain_name, ".", "-")}-lambda"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
data "aws_iam_policy_document" "policy_for_lambda" {
statement {
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
]
resources = [aws_cloudwatch_log_group.lambda-api.arn]
}
}
resource "aws_iam_role_policy" "policy_for_lambda" {
name = "${replace(var.domain_name, ".", "-")}-lambda"
role = aws_iam_role.iam_for_lambda.id
policy = data.aws_iam_policy_document.policy_for_lambda.json
}
resource "aws_cloudwatch_log_group" "lambda-api" {
name = "/aws/lambda/${replace(var.domain_name, ".", "-")}"
retention_in_days = var.log_retention_in_days
}
Variables
Algunas variables necesarias
variable "profile_name" {
default = "default"
}
variable "region" {
default = "us-east-1"
}
variable "email_address_arn" {
default = "arn:aws:ses:us-east-1:894527626897:identity/[email protected]"
}
variable "cognito_user_pool_name" {
default = "api-skynetng-pw"
}
variable "domain_name" {
default = "api.skynetng.pw"
}
variable "log_retention_in_days" {
default = 7
}
variable "function_name" {
description = "Function name"
default = "mylambda"
}
variable "stage_name" {
description = "Api version number"
default = "v1"
}
variable "environment_variables" {
description = "Map with environment variables for the function"
default = {
myenvvar = "test"
}
}
Y por último, el archivo de locales. En este pequeño fragmento, simplemente estamos creando un mapa con un valor calculado y los valores que pueden provenir de una variable, lo cual puede ser muy útil en muchos escenarios donde no se tiene toda la información de antemano o algo se asigna dinámicamente:
locals {
computed_environment_variables = {
"COGNITO_CLIENT_ID" = aws_cognito_user_pool_client.client.id
}
environment_variables = merge(local.computed_environment_variables, var.environment_variables)
}
Scripts de despliegue
Hay un pequeño script en bash para facilitar la ejecución del despliegue, también conocido como compilar el código, comprimirlo en un zip y ejecutar terraform para actualizar nuestra función o lo que sea que hayamos modificado.
#!/bin/bash
set -u
source config.sh
cleanup() {
echo 'Cleaning up'
rm -f lambda.zip
}
create_zip() {
echo 'Zipping lambda'
cd src
go get ./...
go build ./...
mv api.skynetng.pw main
cd ..
zip --junk-paths -r src/main.zip src/main
}
cleanup
create_zip
cd terraform
terraform apply -auto-approve \
-var "region=us-east-1" \
-var "profile_name=${profile_name}" \
-var "domain_name=${domain_name}"
cd ..
Go
Lo bueno es que todo es código, pero no tenemos que gestionar ningún servidor, simplemente consumimos los servicios de AWS directamente desde el código, ¿no es increíble? Disculpen la longitud del archivo, pero van a notar que es muy repetitivo. En la mayoría de las funciones, cargamos la configuración de AWS, hacemos una solicitud y devolvemos una respuesta. También estamos usando Gin como enrutador, que es bastante directo y fácil de usar. Tenemos solo un endpoint autenticado (/user/profile
), y otro sin autenticación que es un chequeo de salud (/app/health
). Los otros dos paths (/user
y /user/validate
) son exclusivamente para el proceso de creación de usuario con cognito.
package main
import (
"context"
"log"
"net/http"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/aws/external"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go/aws"
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
"github.com/gin-gonic/gin"
)
var ginLambda *ginadapter.GinLambda
type User struct {
AccessToken string `json:"access_token"`
}
func getProfile(c *gin.Context) {
user := User{}
err := c.BindJSON(&user)
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
cfg, err := external.LoadDefaultAWSConfig()
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
cognito := cognitoidentityprovider.New(cfg)
req := cognito.GetUserRequest(&cognitoidentityprovider.GetUserInput{
AccessToken: aws.String(user.AccessToken),
})
resp, err := req.Send(c)
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
c.JSON(http.StatusAccepted, gin.H{
"user": resp,
})
}
type UserPassword struct {
Username string `json:"username"`
Password string `json:"password"`
}
func createUser(c *gin.Context) {
user := UserPassword{}
err := c.BindJSON(&user)
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
cfg, err := external.LoadDefaultAWSConfig()
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
cognito := cognitoidentityprovider.New(cfg)
req := cognito.SignUpRequest(&cognitoidentityprovider.SignUpInput{
ClientId: aws.String(os.Getenv("COGNITO_CLIENT_ID")),
Username: aws.String(user.Username),
Password: aws.String(user.Password),
ValidationData: []cognitoidentityprovider.AttributeType{cognitoidentityprovider.AttributeType{Name: aws.String("email")}},
})
resp, err := req.Send(c)
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
c.JSON(http.StatusAccepted, gin.H{
"user": resp,
})
}
type UserValidation struct {
Username string `json:"username"`
Code string `json:"code"`
}
func validateUser(c *gin.Context) {
user := UserValidation{}
err := c.BindJSON(&user)
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
cfg, err := external.LoadDefaultAWSConfig()
if err != nil {
log.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
cognito := cognitoidentityprovider.New(cfg)
req := cognito.ConfirmSignUpRequest(&cognitoidentityprovider.ConfirmSignUpInput{
ClientId: aws.String(os.Getenv("COGNITO_CLIENT_ID")),
Username: aws.String(user.Username),
ConfirmationCode: aws.String(user.Code),
})
resp, err := req.Send(c)
if err != nil {
log.Println(resp, err)
c.JSON(http.StatusInternalServerError, gin.H{
"err": err.Error(),
})
return
}
c.JSON(http.StatusAccepted, gin.H{
"status": "Confirmed",
})
}
func init() {
log.Printf("Gin cold start")
r := gin.Default()
r.POST("/user/validate", validateUser)
r.POST("/user", createUser)
r.POST("/user/profile", getProfile)
r.GET("/app/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "healthy",
})
})
ginLambda = ginadapter.New(r)
}
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
return ginLambda.ProxyWithContext(ctx, req)
}
func main() {
lambda.Start(Handler)
}
Todos los logs se envían a CloudWatch y también podés usar X-Ray para diagnosticar problemas.
Probándolo
Vamos a usar el terminal con curl
para crear, validar y consultar el perfil vacío del usuario en la API.
# Create the account
$ curl https://api.skynetng.pw/user -X POST -d '{ "username": "[email protected]", "password": "Testing123@" }'
OUTPUT:
{
"user": {
"CodeDeliveryDetails": {
"AttributeName": "email",
"DeliveryMedium": "EMAIL",
"Destination": "k***+***t@g***.com"
},
"UserConfirmed": false,
"UserSub": "317e9839-e9ee-4969-855d-1c13ac79662c"
}
}
# Validate the account, this would be normally done from a webapp or mobile app, but since we're not doing the frontend we need a way to test it.
$ curl https://api.skynetng.pw/user/validate -X POST -d '{ "username": "[email protected]", "code": "680641" }'
OUTPUT:
{ "status": "Confirmed" }
# Once the account is confirmed, we craft this file with the login details to get an access token (Authentication).
$ cat auth.json
OUTPUT:
{
"AuthParameters": {
"USERNAME": "[email protected]",
"PASSWORD": "Testing123@"
},
"AuthFlow": "USER_PASSWORD_AUTH",
"ClientId": "4o2gst5o56074cc4af90vpeujk"
}
# Then we issue this curl call to actually get the token.
$ curl -X POST --data @auth.json -H 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' -H 'Content-Type: application/x-amz-json-1.1' https://cognito-idp.us-east-1.amazonaws.com/ | jq
OUTPUT:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4037 100 3883 100 154 3476 137 0:00:01 0:00:01 --:--:-- 3614
{
"AuthenticationResult": {
"AccessToken": "eyJraWQiOiJJMVN1Q0VteVlVXC9OSkFVY2lLOWRNeE1VSUJzTHZDYm9KejBaaGozZG5SND0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI5ZDMzNDk1MC1mOGIzLTRlZjMtOTVlYy0wNWYzODQxN2UxNTEiLCJldmVudF9pZCI6Ijc2MDVjMTI3LTcwMmItNDI3OS04ZWU5LWQyOGUxY2ZiZjVmYSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE1Njc0NjI0NDAsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9uYXdzLmNvbVwvdXMtZWFzdC0xX3IwdWdoOUR1cSIsImV4cCI6MTU2NzQ2NjA0MCwiaWF0IjoxNTY3NDYyNDQwLCJqdGkiOiJlMzE0MmJkMC02ZjQ0LTQyNGMtOTExNy01ZTg3NWZhOTg1MDQiLCJjbGllbnRfaWQiOiI0bzJnc3Q1bzU2MDc0Y2M0YWY5MHZwZXVqayIsInVzZXJuYW1lIjoiOWQzMzQ5NTAtZjhiMy00ZWYzLTk1ZWMtMDVmMzg0MTdlMTUxIn0.TjuOR6naiWKYQvuS3gNM8PJXVlL3wqg6TwNGAHqnJ5HzSRx5sQX2bbLUtY1qB7vwACyqQEdYObgGyc8CpV65yNZ9NeNjnCE4wfJMLpSRNXdTQeDpCqNlLVTC8wN33A_ksq1zqTllXRbSODk6rv3trBMs_phJqpDRdxeWR7fsgOwh8J6BcRxg-LhUYRh_IF7EQpFkbOlDi5MAQiz-8-koHf84r75fs28yIT15LVQWcwYXNoS5mUFYdHxuUKsuagdO5VremsT-Y1NQEcwUwe8JL-UwGtVv18IXHk_qrE8uovJiJ7zDKeuEah6ycI1jgTaGBBVLqCBXgf2Nb5XRJ77BUA",
"ExpiresIn": 3600,
"IdToken": "eyJraWQiOiJKbzNWczRLS0FmcXNtOGFlVVNPSzJcLzdcL2JweGUwNkV2Vk9nRnlcL3Njb2VZPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI5ZDMzNDk1MC1mOGIzLTRlZjMtOTVlYy0wNWYzODQxN2UxNTEiLCJhdWQiOiI0bzJnc3Q1bzU2MDc0Y2M0YWY5MHZwZXVqayIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZXZlbnRfaWQiOiI3NjA1YzEyNy03MDJiLTQyNzktOGVlOS1kMjhlMWNmYmY1ZmEiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTU2NzQ2MjQ0MCwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfcjB1Z2g5RHVxIiwiY29nbml0bzp1c2VybmFtZSI6IjlkMzM0OTUwLWY4YjMtNGVmMy05NWVjLTA1ZjM4NDE3ZTE1MSIsImV4cCI6MTU2NzQ2NjA0MCwiaWF0IjoxNTY3NDYyNDQwLCJlbWFpbCI6ImthaW5saXRlQGdtYWlsLmNvbSJ9.RuwjyG_y4AgBkAVD29scmW8zF3nANGjrDt33v4wOIGAxH0nWbIDc9lMDCS57mOb02LwglyqlsJXGt-BCgjXdKvumjbehAu_a9E3KZlAjA7l4anoSHoIPN_gU5DmiBhL67OTRr4bZxQjTup6abloWt0sqiUx_gA5okH3VNi1oooCIVQ1GfJ53mxhtdUB1LiHpJ7aIwDYDqLFrrNj8f2I4r0oomAkFSEt6DjpEKXI33tCUj9AI-n9JH2wcgsvVAPGIjryfWDrgb8sEujhoZq-AjOHb3ri2B8aWnx0-DQTAVKVnxwBQZH8YAK0r2oLNxhIqZDCUEXaMpzYcDjjG83kA3Q",
"RefreshToken": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.OupuJR3hZUT2bWQTA0n2YqIxN6Lqphxv_yZXjAaJSIKub66xuiF1axUFC9h9SEroWedqR0id_vmfbfH_71_EyS0HZjUlyXSde3WXefY9Yc2YNK1ChCR4rh33r3neuQg0hKx63JTIMVf9yWeFl1Mx4KEsA4eTyvN6GE8o54lGNX6fQX7KovuG5rH0iZQVb2oPFglokAk2uCB4M6p2SbUtWLO5oZYOPWwax62JHb2b5EEwDW4aAJEoWjVetbjLnQHLedYv9Ur68XeSKRtrslZAdCyFjD6Rc1oxuuBEi590CJgj3tlpJV7rSE_185NB90Sa6uU728sksub9sxnANUDY3A.jXKGBg26M4uE3dWm.qO7BO568Aa-4U55mvR-7_1CMCk7ZN7clBAVfKkFyDz6LXwkki9V2ohvm4f_joN88htr4p7AFV7Rik23-vTtTgPXVI9vBUIt37EXyjaMKPJX3B15XnYnC1-YhxjGma6IiyC1oUuiCmUIm9L1c7Jxm8ZmCBFNjc-ARXJTvWAHB3eYHk6U3WHOSB9X_MT2K9Dmrl1-q1vjP4taUKlj60jy91uwk0Ti8tVHp7ETpe4DyEcv9EuoFtEznrJDYcnYpVLyaq9Fkb43F7L3kMq_Jx4IAbWhCaeC35JJa56zAZ1eZ_mGuC5T86xChNqDElhod6m_pz33j5OyPL8rXh8ldERbB7cBB1QgePbPlr1GOvC2T8mE1Rb_gvzNU05Nm1VaeXnClV39GFBpGpEfKY6uvnWiZZICTE3LZRDIhI8Cn-9LwkAUMzqQRKYcPGZswZ68Ma_H40xB3A8pyw_RMF_QvZurUumg-RDeFS2CSnW12zhtMQhr_60Jt9vRQCbWVjBcTh40ZBO5TE8IHsulPq5uJaBEhY2_lpIga4HHjI6cqZ7J3PkRjTc6ZNzAaJiGY0cdRi2pXTeiLqm5_04BUzVbfBQytURbYoaYLxv_wXT-gR4JTLewxJyouO4l955GXK2IjXdZxyFmaXaKrKs7UuiRrkc65_Xzbn8Gj9u0beN661W1CsdymYNht5RfFsJMh85IhsxmzR7XM_2kDVQIjo5EZq5XV0SKQmVjFPYuUIQKcdkx0UsQzQisuuZarcWoDSNq-rxWJF2JbzjtkPyoaSaPxlwC8TL4HXQvT5HQ9S6VeSp0PTQj2BV4NtqzEpskJ9Nql3xrn488WpZD7NBeWkkA_bBKKiYDALoXjmEd6yvmehVtP1VBoqszHOMbikLa1FakJIGpbXqtaH3ZvLdrVCFTKCoEtMob_c4YKoiRgoqyAXew-H_znrxHagVLnJRqfYXLVFkjxGm23PKvSRSRsUXNIlBwUrh8hwL4rsyZLPTE-8aAiG-6VmVE3Xg-JvMtftC-MC5W5PAjLACf_hJP97FrCtg65dUY4-GCGnGMmbe-yLx7z0YaKzuGxccwyT08E-zfmVCeBrkeA-4niUt3xcTJkOOKnPycMWG437cFPxp7sEjw0f4CnZfwX7YHO2rB78UODCZIIqbXgmSQgt86HxNLDqcKY0gLQCIVd3VQSgyRTjOefE3BUGqUHmJ5fKt207tYq_YN6R6rKUvD6NFORmqXIY3AnfU3W1c0FF5ta56T_MW9XSxmcX5AzmT1ZUuzsuNA7gImdu9cpYabynLZgXKSIvcfix__vO84X8bWzr06McPMtRWvLmOr8X5RF9u3X7Q.WVY_SyQ6pF_ae5YP1ov2tg",
"TokenType": "Bearer"
},
"ChallengeParameters": {}
}
# And to validate that we can authenticate users with our code we finally fetch the profile
$ curl https://api.skynetng.pw/user/profile -X POST -d '{ "access_token": "very_long_access_token_from_the_previous_command" }'
OUTPUT:
{
"profile": {
"MFAOptions": null,
"PreferredMfaSetting": null,
"UserAttributes": [
{
"Name": "sub",
"Value": "317e9839-e9ee-4969-855d-1c13ac79662c"
},
{
"Name": "email_verified",
"Value": "false"
},
{
"Name": "email",
"Value": "[email protected]"
}
],
"UserMFASettingList": null,
"Username": "317e9839-e9ee-4969-855d-1c13ac79662c"
},
"user": {
"access_token": "eyJraWQiOiJJMVN1Q0VteVlVXC9OSkFVY2lLOWRNeE1VSUJzTHZDYm9KejBaaGozZG5SND0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI5ZDMzNDk1MC1mOGIzLTRlZjMtOTVlYy0wNWYzODQxN2UxNTEiLCJldmVudF9pZCI6Ijc2MDVjMTI3LTcwMmItNDI3OS04ZWU5LWQyOGUxY2ZiZjVmYSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE1Njc0NjI0NDAsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9uYXdzLmNvbVwvdXMtZWFzdC0xX3IwdWdoOUR1cSIsImV4cCI6MTU2NzQ2NjA0MCwiaWF0IjoxNTY3NDYyNDQwLCJqdGkiOiJlMzE0MmJkMC02ZjQ0LTQyNGMtOTExNy01ZTg3NWZhOTg1MDQiLCJjbGllbnRfaWQiOiI0bzJnc3Q1bzU2MDc0Y2M0YWY5MHZwZXVqayIsInVzZXJuYW1lIjoiOWQzMzQ5NTAtZjhiMy00ZWYzLTk1ZWMtMDVmMzg0MTdlMTUxIn0.TjuOR6naiWKYQvuS3gNM8PJXVlL3wqg6TwNGAHqnJ5HzSRx5sQX2bbLUtY1qB7vwACyqQEdYObgGyc8CpV65yNZ9NeNjnCE4wfJMLpSRNXdTQeDpCqNlLVTC8wN33A_ksq1zqTllXRbSODk6rv3trBMs_phJqpDRdxeWR7fsgOwh8J6BcRxg-LhUYRh_IF7EQpFkbOlDi5MAQiz-8-koHf84r75fs28yIT15LVQWcwYXNoS5mUFYdHxuUKsuagdO5VremsT-Y1NQEcwUwe8JL-UwGtVv18IXHk_qrE8uovJiJ7zDKeuEah6ycI1jgTaGBBVLqCBXgf2Nb5XRJ77BUA"
}
}
He añadido la mayor parte de la información como comentarios en el snippet, ten en cuenta que también usé mi dominio de prueba skynetng.pw
con el subdominio api
para todas las pruebas.
Notas finales
Este artículo fue fuertemente inspirado por este post de Alexander, ¡gracias a él por el excelente trabajo! Este post expande sobre ese y agrega el certificado con ACM, además de manejar una configuración básica de AWS Cognito y el código en Go necesario para hacerlo funcionar. Hay otras formas de lograr lo mismo, pero lo que me gusta de este enfoque es que podés tener algunos endpoints o paths sin autenticación, y usarla según lo necesites, bajo demanda. Este artículo es un poco diferente, pero intentaré reorganizarlo en las próximas semanas y también cubrir más del contenido mostrado aquí. ¡Dejame saber si tenés comentarios o sugerencias!
En un futuro cercano, planeo ampliar este artículo en otro agregando algunas cosas interesantes, por ejemplo, permitir que un usuario suba una imagen a un bucket de S3 y la obtenga con un nombre amigable usando CloudFront (de manera segura, y permitiendo que el usuario sólo pueda subir/actualizar su foto de perfil, pero que pueda ver la foto de perfil de cualquiera). La idea es tener una pequeña API totalmente funcional utilizando servicios de AWS y capacidades serverless con tareas comunes que se encuentran en cualquier sitio web funcional.
Errata
Si encontrás algún error o tenés alguna sugerencia, por favor enviame un mensaje para que lo pueda corregir.
No tienes cuenta? Regístrate aqui
Ya registrado? Iniciar sesión a tu cuenta ahora.
-
Comentarios
Online: 0
Por favor inicie sesión para poder escribir comentarios.