Serverless authentication with Cognito
Introduction
In this article we will see how to use Terraform and Go to create a serverless API using API Gateway, Lambda, and Go, and we will also handle authentication with AWS Cognito, the repo with the files can be found here.
Terraform
In this example I used terraform 0.12, and I kind of liked the new changes, it feels more like coding and a more natural way to describe things, however I think there are more bugs than usual in this version, but I really like the new output for the plan, apply, etc, getting back to the article since there is a lot of code I will gradually update the post with more notes and content or maybe another post explaining another section, but the initial version will only show the cognito part and the code to make it work and how to test it.
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"]
}
As we can see it’s really simple to have a cognito user pool working, the most important part here is the auto_verified_attributes
because that is what makes cognito to actually send an email or an sms with the confirmation code, the rest is self-describing, it creates a pool and a client, since what we need to be able to interact with out pool is the client that part is of considerable importance even that we have most things with default values. As you might have noticed we defined two explicit_auth_flows
and that is to be able to interact with this user pool using user and password.
ACM
Next let’s see how we manage the certificate creation using 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
}
Here basically we create the certificate using aws_acm_certificate
and validate it automatically using the DNS
method and the resource aws_acm_certificate_validation
, the other resources in the file are just there because they are kind of associated but not necessarily need to be there.
Route53
Here we just create an alias record for the API Gateway and the validation record.
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
While this file might seem relatively simple, the API Gateway has many features and can get really complex really fast, basically what we are doing here is creating an API with a resource that accepts all method types and proxy that as it is to our lambda function.
# 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
This file has the lambda function definition, the policy and the roles needed, basically the policy is to be able to log to CloudWatch and to inspect with X-Ray, then the log group to store the logs will set the retention period by default 7 days.
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 and locals
First the variables file with the default values
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"
}
}
And last the locals file, in this small snippet we are just making a map with a computed value and the values that can come from a variable which can be quite useful in many scenarios where you don’t know all the information in advance or something is dynamically assigned:
locals {
computed_environment_variables = {
"COGNITO_CLIENT_ID" = aws_cognito_user_pool_client.client.id
}
environment_variables = merge(local.computed_environment_variables, var.environment_variables)
}
Deployment scripts
There is a small bash script to make it easier to run the deployment, AKA as compiling the code, zipping it, and running terraform to update our function or whatever we changed.
#!/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
The good thing is that everything is code, but we don’t have to manage any server, we just consume services from AWS completely from code, isn’t that amazing?, I apologize for the length of the file, but you will notice that it’s very repetitive, in most functions we load the AWS configuration, we make a request and return a response, we’re also using Gin as a router, which is pretty straight-forward and easy to use, we have only one authenticated path (/user/profile
), and we also have another unauthenticated path which is a health check (/app/health
), the other two paths (/user
and /user/validate
) are exclusively for the user creation process with 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)
}
All logs go to CloudWatch and you can also use X-Ray to diagnose issues.
Testing it
So we’re going to hit the API to create, validate, and query the empty profile of the user from the terminal using curl.
# 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"
}
}
I have added most info in as comments in the snippet, note that I also used my test domain skynetng.pw
with the subdomain api
for all tests.
Closing notes
This post was heavily inspired by this post from Alexander, kudos to him for the great work!, this post expands on that and adds the certificate with ACM, it also handles a basic AWS Cognito configuration and the necessary go code to make it work, there are other ways to accomplish the same, but what I like about this approach is that you can have some endpoints or paths without authentication and you can use authentication, etc on-demand. This article is a bit different but I will try to re-shape it in the following weeks, and also cover more of the content displayed here, let me know if you have any comments or suggestions!
In some near future I will build upon this article in another article adding a few cool things, for example to allow an user to upload an image to an S3 bucket and fetch that with a friendly name using Cloudfront (In a secure manner, and only able to upload/update his/her profile picture, while being able to fetch anyone profile pic), the idea is to have a fully functional small API using AWS services and serverless facilities with common tasks that you can find in any functional website.
Errata
If you spot any error or have any suggestion, please send me a message so it gets fixed.
Also, you can check the source code and changes in the generated code and the sources here
-
Comments
Online: 0
Please sign in to be able to write comments.