Lambda Authorizer — PoC

Jose Figueredo
AWS Tip
Published in
6 min readNov 19, 2023

--

In this article I will guide you through a simple proof of concept that shows how to secure a lambda with another lambda that works as the authorizer

Requirements

  • Serverless framework installed
  • AWS CLI installed and configured
  • Go installed

Components

  • Protected Lambda
  • Authorize Lambda
  • Authentication Lambda
  • Private & public key generator utility

Serverless specification

file: ./serverless.yaml

service: hello-world

provider:
name: aws
runtime: go1.x
region: us-east-1
memorySize: 128

package:
exclude:
- ./**
include:
- ./bin/**

functions:
authenticator:
handler: bin/authenticator
events:
- http:
path: /auth
method: post
authorizer:
handler: bin/authorizer
hello-world:
handler: bin/hello-world
events:
- http:
path: /me
method: get
authorizer:
name: authorizer
resultTtlInSeconds: 300

Make file

file: ./Makefile

help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

functions := $(shell find functions -name \*main.go | awk -F'/' '{print $$2}')

build: ## Build golang binaries
@for function in $(functions) ; do \
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$$function functions/$$function/main.go ; \
done

Utility

To complete the PoC we need a private key in pem format and a public key in jwks format.

file: ./utility/jwks-generator/main.go

package main

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"math/big"
"time"

"github.com/go-jose/go-jose/v3"
)

func main() {

// Parse key_id from arguments
var keyId string
flag.StringVar(&keyId, "key_id", "123456789ABCDEF", "Key ID")
flag.Parse()
if keyId == "" {
panic("Missing keyId")
}

privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
pemdata := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
},
)
println(string(pemdata))

jwks := generateJWTWithKeyID(privateKey, &keyId)
jwksJSON, err := jwks.Keys[0].MarshalJSON()
if err != nil {
panic(err)
}
println(string(jwksJSON))
}

func generateJWTWithKeyID(privateKey *rsa.PrivateKey, keyId *string) *jose.JSONWebKeySet {

serialNumber, err := rand.Int(rand.Reader, big.NewInt(100))
if err != nil {
panic(err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Itti Digital"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(2 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
panic(err)
}
certificate, err := x509.ParseCertificate(derBytes)
if err != nil {
panic(err)
}

return &jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{
{
Certificates: []*x509.Certificate{certificate},
Key: &privateKey.PublicKey,
KeyID: *keyId,
Use: "sig",
},
},
}
}

Running the JWKs generator

go run ./utility/jwks-generator/main.go

Output: the private key in PEM format

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqilOI30+hZvSu2KVioTPaaUmNZSWXsbmz3FR1GuD9vO4utZ3
uNxz2pOe0OMT0ZpsgoBmHVH52F72dJZ1eJRkBY+wmlWeuYj3dCPV+xQAgaOxQtRJ
7Epi95QTpAEVAJeKJDX22TyU6x4q4aNzpglXbT1umFCQjZH0mgyPlwK1Xieqai9k
abWcJFKpLmXAUnDSR7ClEQbB1fvg1CB6S4aWE7Mx17VGZFom3fFO0xC0rzByyIIi
qD2GitfiSrZ2bOsfLCj9q9TmcgJiJuQPUAOCHnOWO/14juMkBtttOvHr5u4sGO/0
8N4ZsX3gplbONv5imLJiOs2xbAbnm5l9d3GnfwIDAQABAoIBAAg8X4/QLAqDdDul
ld9SdkeCusq1GmIT9m/r6C4D1itJuJMydjD4WpMlufYaR4dJlh7q4AZjRVh3oC4c
aisf44dxYPbXVgtc2b1BTsYMPcoIhjfZ1oodP5UEEb9KXh3dN85w3jW9fOe0Whb5
tks/AIBFDOlKXPS9L72VBg4lD2ozJQ8oyQVfC5ta2Ek5zBHDsHlYYb0dLFCvcRit
f4mXAiEmZhlwdyS+pnI1OdXLyAxfYyf37+d6YtmoxTUDvdgFtfN6oc83a39S4jhF
dAVtf6PIr74qjEXCSlTAC1VNE+2z3+gXew14dGw5Qa6DyifS6j5/LdaktBOcqa5w
nFRyP4ECgYEA2idEL6s6PxKH/MNys+BDH63USJkPxeQ28Ny2yLZLus2PDiIgYpvP
5g/Y0ZSAw/HpAEK3w6Grzd1PfBqPpHUsUuxHt5q5gBPfXcgGY8Wuu7/3yiC7LEkH
5o+n5RuPsV7M0qSm6rVhELHjMihG/Z2mN5RMK8ebeG306ZHDCSsUbY8CgYEAx66X
5xeTfxhfhlGZ2C8M6WXRIuKVstYGplpPhwstpLQuh8xLj8BTHYxEES1SdHB+CvKn
1tdAgSX6H/225GNIKXMpe4Gsc7R2LmfubvCLPwCD5/ZFpvY8FItY+QCuo4xXxuUA
JpBuWvDxC3wBjnN8zZtdys1hx/p1pArxq70lDxECgYEAwD1KARfKxDn4S+2P7qL5
g7kTEMaQ97ocEDTvff/mzD7IiZPZJgxYMExWrJlIv2M2CFzCw0p8s3UKzjo5yprW
7Fv69vkJ7quUcngJ6XISgLCyExS03Fme9LYzJdobzhnUNOuTi9E6MBQSOej0Zhm+
l8u/M/U6M+3xnMIuNK4Z2lECgYAN9GGhQMCKDUX/uQwrU35vgTIQYg1cJiDo1z7f
jSRvcjgePWS6cxJb6kWHfcdZY9MrKLIaDYjwfZrxSWXSqC2O2AF6JCHNJDtuGs1K
63yPtpWBTHCprmOce/CH1kheHZy0xaQxDb7olBYEW3IwZlm+dLElTx0aQKKgCDPD
cMB6QQKBgH0OvCbLzFo/Ju40zfincEhzIISDcfuruxRdFX4tCD5dBR73blaQLRnG
arEZGwpZUPegPLTEZg9VnaL/RM8TdlCzJvIHwik1/N+aYHeY0IJYzt2uGKptd8Fs
snz+sXJFZAZsq7aqKiACBbUMsNdTJAI9BIx4FeJdwPrf0HTrxG+f
-----END RSA PRIVATE KEY-----

Output: JWKs in JSON format

{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "123456789ABCDEF",
"n": "qilOI30-hZvSu2KVioTPaaUmNZSWXsbmz3FR1GuD9vO4utZ3uNxz2pOe0OMT0ZpsgoBmHVH52F72dJZ1eJRkBY-wmlWeuYj3dCPV-xQAgaOxQtRJ7Epi95QTpAEVAJeKJDX22TyU6x4q4aNzpglXbT1umFCQjZH0mgyPlwK1Xieqai9kabWcJFKpLmXAUnDSR7ClEQbB1fvg1CB6S4aWE7Mx17VGZFom3fFO0xC0rzByyIIiqD2GitfiSrZ2bOsfLCj9q9TmcgJiJuQPUAOCHnOWO_14juMkBtttOvHr5u4sGO_08N4ZsX3gplbONv5imLJiOs2xbAbnm5l9d3Gnfw",
"e": "AQAB",
"x5c": [
"MIIC3jCCAcagAwIBAgIBRDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQKEwxJdHRpIERpZ2l0YWwwHhcNMjMxMTEzMTk1NDIwWhcNMjMxMTEzMjE1NDIwWjAXMRUwEwYDVQQKEwxJdHRpIERpZ2l0YWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqKU4jfT6Fm9K7YpWKhM9ppSY1lJZexubPcVHUa4P287i61ne43HPak57Q4xPRmmyCgGYdUfnYXvZ0lnV4lGQFj7CaVZ65iPd0I9X7FACBo7FC1EnsSmL3lBOkARUAl4okNfbZPJTrHirho3OmCVdtPW6YUJCNkfSaDI+XArVeJ6pqL2RptZwkUqkuZcBScNJHsKURBsHV++DUIHpLhpYTszHXtUZkWibd8U7TELSvMHLIgiKoPYaK1+JKtnZs6x8sKP2r1OZyAmIm5A9QA4Iec5Y7/XiO4yQG22068evm7iwY7/Tw3hmxfeCmVs42/mKYsmI6zbFsBuebmX13cad/AgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCCXmiPBPZs6ZXFtpOkLs1HpULnehPHkxc8p5jlf/J0UBoouCeH9/Z6NvrVyob/zDW1xJrrtfZ44CQo2twg0y2Yb6J8XFE4H69B0lqEcNf4ODA6J3Rle3GyrSKFSrvj26YQ+2HTPUvBOjiXp9GHWnVZLRuRWVg7hTfWsQkafk0OgCKa7YSJujFZxgKpFqzPWZGxwsELrLe0GA51JlEUTzNZnXqG4FuTmn0sgRARECSM/TdBEOnIVBEiOfgdlyxBNHHXd6p3O1FqyLsO+mtdEj1tLUNTYqbYsUbWs2/Z2zUQSvb3Og4gHe9IIhAyoWb8alZEmDlyjmhTuQtXFcrOvmIl"
]
}
]
}

Authenticator Lambda

For simplicity we will hard code the private PEM key as a constant and use an API Key to prevent calls from clients that are not valid consumers.

package main

import (
"encoding/json"
stdlog "log"
"net/http"
"os"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/golang-jwt/jwt/v5"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
)

var version = "1.0.3"

var ApiKey = "XAUnDSR7ClEQbB1"

var kid = "123456789ABCDEF"

var privateKeyPEMString = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqilOI30+hZvSu2KVioTPaaUmNZSWXsbmz3FR1GuD9vO4utZ3
uNxz2pOe0OMT0ZpsgoBmHVH52F72dJZ1eJRkBY+wmlWeuYj3dCPV+xQAgaOxQtRJ
7Epi95QTpAEVAJeKJDX22TyU6x4q4aNzpglXbT1umFCQjZH0mgyPlwK1Xieqai9k
abWcJFKpLmXAUnDSR7ClEQbB1fvg1CB6S4aWE7Mx17VGZFom3fFO0xC0rzByyIIi
qD2GitfiSrZ2bOsfLCj9q9TmcgJiJuQPUAOCHnOWO/14juMkBtttOvHr5u4sGO/0
8N4ZsX3gplbONv5imLJiOs2xbAbnm5l9d3GnfwIDAQABAoIBAAg8X4/QLAqDdDul
ld9SdkeCusq1GmIT9m/r6C4D1itJuJMydjD4WpMlufYaR4dJlh7q4AZjRVh3oC4c
aisf44dxYPbXVgtc2b1BTsYMPcoIhjfZ1oodP5UEEb9KXh3dN85w3jW9fOe0Whb5
tks/AIBFDOlKXPS9L72VBg4lD2ozJQ8oyQVfC5ta2Ek5zBHDsHlYYb0dLFCvcRit
f4mXAiEmZhlwdyS+pnI1OdXLyAxfYyf37+d6YtmoxTUDvdgFtfN6oc83a39S4jhF
dAVtf6PIr74qjEXCSlTAC1VNE+2z3+gXew14dGw5Qa6DyifS6j5/LdaktBOcqa5w
nFRyP4ECgYEA2idEL6s6PxKH/MNys+BDH63USJkPxeQ28Ny2yLZLus2PDiIgYpvP
5g/Y0ZSAw/HpAEK3w6Grzd1PfBqPpHUsUuxHt5q5gBPfXcgGY8Wuu7/3yiC7LEkH
5o+n5RuPsV7M0qSm6rVhELHjMihG/Z2mN5RMK8ebeG306ZHDCSsUbY8CgYEAx66X
5xeTfxhfhlGZ2C8M6WXRIuKVstYGplpPhwstpLQuh8xLj8BTHYxEES1SdHB+CvKn
1tdAgSX6H/225GNIKXMpe4Gsc7R2LmfubvCLPwCD5/ZFpvY8FItY+QCuo4xXxuUA
JpBuWvDxC3wBjnN8zZtdys1hx/p1pArxq70lDxECgYEAwD1KARfKxDn4S+2P7qL5
g7kTEMaQ97ocEDTvff/mzD7IiZPZJgxYMExWrJlIv2M2CFzCw0p8s3UKzjo5yprW
7Fv69vkJ7quUcngJ6XISgLCyExS03Fme9LYzJdobzhnUNOuTi9E6MBQSOej0Zhm+
l8u/M/U6M+3xnMIuNK4Z2lECgYAN9GGhQMCKDUX/uQwrU35vgTIQYg1cJiDo1z7f
jSRvcjgePWS6cxJb6kWHfcdZY9MrKLIaDYjwfZrxSWXSqC2O2AF6JCHNJDtuGs1K
63yPtpWBTHCprmOce/CH1kheHZy0xaQxDb7olBYEW3IwZlm+dLElTx0aQKKgCDPD
cMB6QQKBgH0OvCbLzFo/Ju40zfincEhzIISDcfuruxRdFX4tCD5dBR73blaQLRnG
arEZGwpZUPegPLTEZg9VnaL/RM8TdlCzJvIHwik1/N+aYHeY0IJYzt2uGKptd8Fs
snz+sXJFZAZsq7aqKiACBbUMsNdTJAI9BIx4FeJdwPrf0HTrxG+f
-----END RSA PRIVATE KEY-----`)

var Logger = createLogger()

func createLogger() *zerolog.Logger {
return createJsonLogger()
}

// ----------------------------------------------------------------------------------------------------------------------
// JSON Logger
func createJsonLogger() *zerolog.Logger {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.SetGlobalLevel(zerolog.InfoLevel)
logger := zerolog.New(os.Stdout).With().Timestamp().Caller().Logger()
stdlog.SetFlags(0)
stdlog.SetOutput(logger)

return &logger
}

// ----------------------------------------------------------------------------------------------------------------------
//
func main() {
lambda.Start(handler)
}

type Request struct {
ApiKey string `json:"api_key"`
SourceAppID string `json:"source_app_id"`
DestinationAppID string `json:"destination_app_id"`
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
log.Logger.Info().Msgf("Version: %s", version)

requestJson, _ := json.Marshal(request)
log.Logger.Debug().Msgf("request: %v", requestJson)

// Get the request body
var req Request
err := json.Unmarshal([]byte(request.Body), &req)
if err != nil {
log.Logger.Err(err).Msg("failed to unmarshal the request body")
return events.APIGatewayProxyResponse{
Body: "ERR-01",
StatusCode: http.StatusBadRequest,
}, nil
}

// Check the API key
if req.ApiKey != ApiKey {
log.Logger.Error().Msg("invalid API key")
return events.APIGatewayProxyResponse{
Body: "ERR-02",
StatusCode: http.StatusUnauthorized,
}, nil
}

// Create the JWT
now := time.Now()

token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iat": jwt.NewNumericDate(now.Add(-time.Minute)),
"exp": jwt.NewNumericDate(now.Add(5 * time.Minute)),
"iss": "authenticator-v1",
"aud": req.SourceAppID,
"sub": req.DestinationAppID,
})
token.Header["kid"] = kid

privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEMString)
if err != nil {
log.Logger.Err(err).Msg("failed to parse the RSA private key")
return events.APIGatewayProxyResponse{
Body: "ERR-03",
StatusCode: http.StatusUnauthorized,
}, nil
}

tokenString, err := token.SignedString(privateKey)
if err != nil {
log.Logger.Err(err).Msg("failed to sign the JWT")
return events.APIGatewayProxyResponse{
Body: "ERR-04",
StatusCode: http.StatusUnauthorized,
}, nil
}

log.Logger.Debug().Msg("token created")

return events.APIGatewayProxyResponse{
Body: tokenString,
StatusCode: http.StatusOK,
}, nil
}

Output: JWT

eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1Njc4OUFCQ0RFRiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhcHBfMSIsImV4cCI6MTY5OTk2OTcwNCwiaWF0IjoxNjk5OTY5MzQ0LCJpc3MiOiJhdXRoZW50aWNhdG9yLXYxIiwic3ViIjoiaGVsbG9fd29sZCJ9.lSKTpbGfBqaxKO65DEbZQUdZ4RH0kZYOQpKsV5FWIWNLIjRaP228qmJi2_ysyTufgvG73Yyv-7C85Ki7fB2wWl-CUob5GhHOlAvVQL9lAirqKZH97fYPYt71mxsOihDQhStfasJTiUIi0eKDguo3Az1MIml40OGoXg39ImYQZfJeOfH0QVbQ2LEr8LJ4rrF1eEIuzApt5LdEJUCJn_qi57lHlheKUu2Dsa3UFDn-XzxcbPjBbMXDJJ6tk9h9GKx6pJD8lWRWcc-5r9ZHxviKjWmuqMfkDA5atpJmpYZAT_ZLl1A1-UJimZUPACBDVHxaTSk6aQM_WcMwW9msHJLIUA

Authorizer Lambda

This lambda will be automatically when ever we call the helloworld lambda as defined in the serverless specification file.

It has the JWKs embedded for simplicity and the input JWT will be validated against it. If everything is fine the lambda will output an allow policy and if it’s not ti will output an explicit deny policy.

package main

import (
"encoding/json"
"errors"
stdlog "log"
"os"
"strings"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"

"github.com/golang-jwt/jwt/v4"

"github.com/MicahParks/keyfunc"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
)

var version = "1.0.3"

var jwksString = []byte(`{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "123456789ABCDEF",
"n": "qilOI30-hZvSu2KVioTPaaUmNZSWXsbmz3FR1GuD9vO4utZ3uNxz2pOe0OMT0ZpsgoBmHVH52F72dJZ1eJRkBY-wmlWeuYj3dCPV-xQAgaOxQtRJ7Epi95QTpAEVAJeKJDX22TyU6x4q4aNzpglXbT1umFCQjZH0mgyPlwK1Xieqai9kabWcJFKpLmXAUnDSR7ClEQbB1fvg1CB6S4aWE7Mx17VGZFom3fFO0xC0rzByyIIiqD2GitfiSrZ2bOsfLCj9q9TmcgJiJuQPUAOCHnOWO_14juMkBtttOvHr5u4sGO_08N4ZsX3gplbONv5imLJiOs2xbAbnm5l9d3Gnfw",
"e": "AQAB",
"x5c": [
"MIIC3jCCAcagAwIBAgIBRDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQKEwxJdHRpIERpZ2l0YWwwHhcNMjMxMTEzMTk1NDIwWhcNMjMxMTEzMjE1NDIwWjAXMRUwEwYDVQQKEwxJdHRpIERpZ2l0YWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqKU4jfT6Fm9K7YpWKhM9ppSY1lJZexubPcVHUa4P287i61ne43HPak57Q4xPRmmyCgGYdUfnYXvZ0lnV4lGQFj7CaVZ65iPd0I9X7FACBo7FC1EnsSmL3lBOkARUAl4okNfbZPJTrHirho3OmCVdtPW6YUJCNkfSaDI+XArVeJ6pqL2RptZwkUqkuZcBScNJHsKURBsHV++DUIHpLhpYTszHXtUZkWibd8U7TELSvMHLIgiKoPYaK1+JKtnZs6x8sKP2r1OZyAmIm5A9QA4Iec5Y7/XiO4yQG22068evm7iwY7/Tw3hmxfeCmVs42/mKYsmI6zbFsBuebmX13cad/AgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCCXmiPBPZs6ZXFtpOkLs1HpULnehPHkxc8p5jlf/J0UBoouCeH9/Z6NvrVyob/zDW1xJrrtfZ44CQo2twg0y2Yb6J8XFE4H69B0lqEcNf4ODA6J3Rle3GyrSKFSrvj26YQ+2HTPUvBOjiXp9GHWnVZLRuRWVg7hTfWsQkafk0OgCKa7YSJujFZxgKpFqzPWZGxwsELrLe0GA51JlEUTzNZnXqG4FuTmn0sgRARECSM/TdBEOnIVBEiOfgdlyxBNHHXd6p3O1FqyLsO+mtdEj1tLUNTYqbYsUbWs2/Z2zUQSvb3Og4gHe9IIhAyoWb8alZEmDlyjmhTuQtXFcrOvmIl"
]
}
]
}`)

var Logger = createLogger()

func createLogger() *zerolog.Logger {
return createJsonLogger()
}

// ----------------------------------------------------------------------------------------------------------------------
// JSON Logger
func createJsonLogger() *zerolog.Logger {
//zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.SetGlobalLevel(zerolog.InfoLevel)
logger := zerolog.New(os.Stdout).With().Timestamp().Caller().Logger()
stdlog.SetFlags(0)
stdlog.SetOutput(logger)

return &logger
}

// ----------------------------------------------------------------------------------------------------------------------
//
func main() {
lambda.Start(handler)
}

func handler(request events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) {
log.Logger.Info().Msgf("Version: %s", version)

requestJson, _ := json.Marshal(request)
log.Logger.Debug().Msgf("Request: %s", string(requestJson))

// JWKS
jwks, err := keyfunc.NewJSON(jwksString)
if err != nil {
log.Logger.Err(err).Msg("failed to create JWKS from JSON file")
return events.APIGatewayCustomAuthorizerResponse{}, errors.New("missing or invalid JWKs")
}

// Get the JWT from the header
tokenString := request.AuthorizationToken
tokenSlice := strings.Split(tokenString, " ")

var bearerToken string
if len(tokenSlice) > 1 {
bearerToken = tokenSlice[len(tokenSlice)-1]
}
if bearerToken == "" {
log.Logger.Error().Msg("missing bearer token")
return generateDenyPolicy(request.MethodArn, nil), nil
}

// Parse the JWT
token, err := jwt.Parse(bearerToken, jwks.Keyfunc)
if err != nil {
log.Logger.Err(err).Msg("failed to parse the JWT")
return generateDenyPolicy(request.MethodArn, token), nil
}

// Check if the token is valid
if !token.Valid {
log.Logger.Error().Msg("the token is not valid")
return generateDenyPolicy(request.MethodArn, token), nil
}

log.Logger.Debug().Msgf("the token is valid")
return generateAllowPolicy(request.MethodArn, token), nil
}

func generateAllowPolicy(resource string, token *jwt.Token) events.APIGatewayCustomAuthorizerResponse {
return generatePolicy("user", "Allow", resource, token)
}

func generateDenyPolicy(resource string, token *jwt.Token) events.APIGatewayCustomAuthorizerResponse {
return generatePolicy("user", "Deny", resource, token)
}

func generatePolicy(principalID, effect, resource string, token *jwt.Token) events.APIGatewayCustomAuthorizerResponse {
log.Logger.Debug().Msgf("principalID: %s, effect: %s, resource: %s", principalID, effect, resource)

authResponse := events.APIGatewayCustomAuthorizerResponse{PrincipalID: principalID}

if effect != "" && resource != "" {
authResponse.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{
Version: "2012-10-17",
Statement: []events.IAMPolicyStatement{
{
Action: []string{"execute-api:Invoke"},
Effect: effect,
Resource: []string{resource},
},
},
}
}

if token != nil {
authResponse.Context = token.Claims.(jwt.MapClaims)
}

return authResponse
}

HelloWorld Lambda

This protected lambda will show the name of the audience that's in the authorize request context.

package main

import (
"encoding/json"
"fmt"
"log"
"net/http"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)

func main() {
lambda.Start(handler)
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
requestJson, _ := json.Marshal(request)
log.Println(string(requestJson))

var name string
if _, ok := request.RequestContext.Authorizer["aud"]; ok {
name = request.RequestContext.Authorizer["aud"].(string)
} else {
log.Println("Failed to get name from authorizer.")
return events.APIGatewayProxyResponse{
Body: "Unauthorized: Must be an authorized user with a name",
StatusCode: http.StatusUnauthorized,
}, nil
}

log.Printf("Hello, %s!", name)
return events.APIGatewayProxyResponse{
Body: fmt.Sprintf("Hello, %s!", name),
StatusCode: http.StatusOK,
}, nil
}

Building

Using the Make framework to build will generate the binaries for the lambda in Linux-x64 format in the ./bin directory

make build

Deploy

Using the Serverless framework we deploy the lambdas and the api gateway that routes form internet to the lambdas

sls deploy

Testing

To test the hole process first we need to obtain a valid JWT. For that we will use the hard coded API Key (as a minimal security measure, not to be used in production as is), a source and a destination app ids.

curl -X POST https://wgi26pt000.execute-api.us-east-1.amazonaws.com/dev/auth \
-H "Content-Type: application/json" \
-d '{
"api_key": "XAUnDSR7ClEQbB1",
"source_app_id": "app_1",
"destination_app_id": "hello_wold"
}'

Output: valid JWT

eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1Njc4OUFCQ0RFRiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhcHBfMSIsImV4cCI6MTY5OTk2OTcwNCwiaWF0IjoxNjk5OTY5MzQ0LCJpc3MiOiJhdXRoZW50aWNhdG9yLXYxIiwic3ViIjoiaGVsbG9fd29sZCJ9.lSKTpbGfBqaxKO65DEbZQUdZ4RH0kZYOQpKsV5FWIWNLIjRaP228qmJi2_ysyTufgvG73Yyv-7C85Ki7fB2wWl-CUob5GhHOlAvVQL9lAirqKZH97fYPYt71mxsOihDQhStfasJTiUIi0eKDguo3Az1MIml40OGoXg39ImYQZfJeOfH0QVbQ2LEr8LJ4rrF1eEIuzApt5LdEJUCJn_qi57lHlheKUu2Dsa3UFDn-XzxcbPjBbMXDJJ6tk9h9GKx6pJD8lWRWcc-5r9ZHxviKjWmuqMfkDA5atpJmpYZAT_ZLl1A1-UJimZUPACBDVHxaTSk6aQM_WcMwW9msHJLIUA

Calling the hello world lambda with a header request using the valid JWT

curl https://wgi26pt000.execute-api.us-east-1.amazonaws.com/dev/me \
-H "Authorization: bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1Njc4OUFCQ0RFRiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhcHBfMSIsImV4cCI6MTY5OTk2OTcwNCwiaWF0IjoxNjk5OTY5MzQ0LCJpc3MiOiJhdXRoZW50aWNhdG9yLXYxIiwic3ViIjoiaGVsbG9fd29sZCJ9.lSKTpbGfBqaxKO65DEbZQUdZ4RH0kZYOQpKsV5FWIWNLIjRaP228qmJi2_ysyTufgvG73Yyv-7C85Ki7fB2wWl-CUob5GhHOlAvVQL9lAirqKZH97fYPYt71mxsOihDQhStfasJTiUIi0eKDguo3Az1MIml40OGoXg39ImYQZfJeOfH0QVbQ2LEr8LJ4rrF1eEIuzApt5LdEJUCJn_qi57lHlheKUu2Dsa3UFDn-XzxcbPjBbMXDJJ6tk9h9GKx6pJD8lWRWcc-5r9ZHxviKjWmuqMfkDA5atpJmpYZAT_ZLl1A1-UJimZUPACBDVHxaTSk6aQM_WcMwW9msHJLIUA"

Output: app_1 as audience

Hello, app_1!

--

--