go |aws |jwt

Go Notes: Auth0 validation for AWS Lambda Pt 2

uris77

Roberto Guerra

Posted on March 3, 2020

Go Notes: Auth0 validation for AWS Lambda Pt 2

We will use the auth0 library we forked previously, to create an AWS Lambda(lambda) authorizer. This authorizer will indicate whether a request is authorized or denied. We'll also deploy the authorizer using the serverless framework and write a basic unit test for the authorizer.

Requirements

  • Basic understanding of what jwt is.
  • Some basic Go knowledge.
  • Some basic AWS Lambda knowledge.
  • Some basic serverless knowledge.

Step 1: Configure the Lambda Authorizer

The lambda authorizer is defined in the serverless.yaml file under the functions block. It is no different from any other lambda, except that we won't provide any events. The Auth0 library we will use makes sure that the issuer and the audience match what is in the jwks key. We also need to provide the url that the auth0 client will use to download the jwks key. We'll provide these values as environment variables:

functions:
  authorizer:
    handler: bin/authorizer
    environment:
      AUTH_ISSUER: ${self:custom.authorizer.TOKEN_ISSUER}
      AUTH_AUDIENCE: ${self:custom.authorizer.AUDIENCE}
      JWKS_URI: ${self:custom.authorizer.JWKS_URI}

custom:
  stage: ${opt:stage, "dev"}
  authorizer: ${file(../../config/authorizer.${opt:stage,self:provider.stage}.yml)}
Enter fullscreen mode Exit fullscreen mode

bin/authorizer is the eventually compiled asset that will be uploaded to AWS.

The authorizer variables are maintained in a yaml file under a config folder following the pattern: authorizer.<stage>.yml. Example content:

TOKEN_ISSUER: https://yourapp.auth0.com/
AUDIENCE: your-audience
JWKS_URI: https://yourapp.auth0.com./.well-known/jwks.json
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize the Auth0 Client and logger

We'll initialize the auth0 client and the logger in the init function of our main.go. The reason for doing this is that the AWS Lambda go runtime will keep an instance of our program running after it finishes so that it can re-use them when a request hits the warm instance. This allows us to limit instantiations:

package main

import (
    log "github.com/sirupsen/logrus"
    "github.com/uris77/auth0"
)

var auth0Client auth0.Auth0

func init() {
    // Instantiate an auth0 client with a Cache with the capacity for
    // 60 tokens and a ttl of 24 hours
    auth0Client = auth0.NewAuth0(60, 518400)
    log.SetFormatter(&log.JSONFormatter{})
    log.SetOutput(os.Stdout)
}

Enter fullscreen mode Exit fullscreen mode

The logger is configured to format its output as JSON and to write to Stdout. Lambda will push all stdout to Cloud Watch. This allows us to view our logs as structured data.

Step 3: Gather Preconditions

The preconditions for this authorizer are that we need to have values for AUTH_AUDIENCE, JWKS_URI and AUTH_ISSUER:

func Authorizer(ctx context.Context, evt events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) {
    if len(os.Getenv("JWKS_URI")) < 1 {
        panic("The required JWKS_URI is missing")
    }
    jwkUrl := os.Getenv("JWKS_URI")

    if len(os.Getenv("AUTH_AUDIENCE")) < 1 {
        panic("The required AUTH_AUDIENCE is missing")
    }
    aud := os.Getenv("AUTH_AUDIENCE")

    if len(os.Getenv("AUTH_ISSUER")) < 1 {
        panic("The required AUTH_ISSUER is missing")
    }
    iss := os.Getenv("AUTH_ISSUER")

        log.WithFields(log.Fields{
        "event":       evt,
        "jwkUrl":      jwkUrl,
        "aud":         aud,
        "iss":         iss,
        "auth0Client": auth0Client,
        "context":     ctx,
    }).Info("Authorizer triggered")

}
Enter fullscreen mode Exit fullscreen mode

If all the preconditions are true, we log the initial state. This can be helpful when troubleshooting because we might want to know what were the inputs and the initial state to the authorizer.

Step 4: Validate the token

The authorization token is in the events.APIGatewayCustomAuthorizerRequest parameter of the authorizer:

type APIGatewayCustomAuthorizerRequest struct {
    Type               string `json:"type"`
    AuthorizationToken string `json:"authorizationToken"`
    MethodArn          string `json:"methodArn"`
}
Enter fullscreen mode Exit fullscreen mode

The token is a string and also includes the Bearer string.

    jwt, err := auth0Client.Validate(jwkUrl, aud, iss, evt.AuthorizationToken)

Enter fullscreen mode Exit fullscreen mode

The auth0 client will return a jwt token or an error when we validate a token against the jwkUrl and the audience and issuer.

The authorizer needs to return an ApiGatewayCustomAuthorizerResponse, which will indicate to the Api Gateway if the request is authorized. The type is defined as:

type APIGatewayCustomAuthorizerResponse struct {
    PrincipalID        string                           `json:"principalId"`
    PolicyDocument     APIGatewayCustomAuthorizerPolicy `json:"policyDocument"`
    Context            map[string]interface{}           `json:"context,omitempty"`
    UsageIdentifierKey string                           `json:"usageIdentifierKey,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

The PolicyDocument is what we use to authorize the request. It will include an Iam Policy Statement:

// APIGatewayCustomAuthorizerPolicy represents an IAM policy
type APIGatewayCustomAuthorizerPolicy struct {
    Version   string
    Statement []IAMPolicyStatement
}

// IAMPolicyStatement represents one statement from IAM policy with action, effect and resource
type IAMPolicyStatement struct {
    Action   []string
    Effect   string
    Resource []string
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Error Handling

If validation failed, we can create a response with a policy that will deny the request:

if err != nil {
        r := events.APIGatewayCustomAuthorizerResponse{
            PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
                Version: "2012-10-17",
                Statement: []events.IAMPolicyStatement{
                    {
                        Action:   []string{"execute-api:Invoke"},
                        Effect:   "Deny",
                        Resource: []string{"arn:aws:execute-api:*"},
                    },
                },
            },
        }

        log.WithFields(log.Fields{
            "jwt":      jwt,
            "err":      err,
            "response": r,
        }).Error("Token Validation Failed")

        return r, nil
    }
Enter fullscreen mode Exit fullscreen mode

It is a good idea to also log the error in case we need to do some troubleshooting.

We can also make the policy more specific and use the arn for the request:


r := events.APIGatewayCustomAuthorizerResponse{
            PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
                Version: "2012-10-17",
                Statement: []events.IAMPolicyStatement{
                    {
                        Action:   []string{"execute-api:Invoke"},
                        Effect:   "Deny",
                        Resource: []string{evt.methodArn},
                    },
                },
            },
        }

Enter fullscreen mode Exit fullscreen mode

Step 6: Happy Path

When validation succeeds, we only need to make two minor changes: provide a PrincipalId and change the Policy Document so that it accepts the request:

resp := events.APIGatewayCustomAuthorizerResponse{
        PrincipalID: jwt.Subject(),
        PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
            Version: "2012-10-17",
            Statement: []events.IAMPolicyStatement{
                {
                    Action:   []string{"execute-api:Invoke"},
                    Effect:   "Allow",
                    Resource: []string{evt.methodArn},
                },
            },
        },
    }

    log.WithFields(log.Fields{
        "jwt":      jwt,
        "response": resp,
    }).Info("Successfully Validated Token")

    return resp, nil


Enter fullscreen mode Exit fullscreen mode

Conclusion

An AWS Lambda Authorizer is the mechanism used by Api Gateway to authorize requests. It will provide an APIGatewayCustomAuthorizerRequest to the authorizer. This request contains both the token and the method's arn.

We can validate the request's token with the auth0 client, and return the corresponding IAM Policy Document, which will indicate to Api Gateway if the request is allowed to be executed.

💖 💪 🙅 🚩
uris77
Roberto Guerra

Posted on March 3, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related