Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / AWS

Building Auth Endpoint with Go and AWS Lambda

5.00/5 (3 votes)
3 Aug 2022CPOL4 min read 12.2K  
This article describes a super-minimalistic auth endpoint based on AWS Lambda.
Serverless is a great option for smallish nanoservices. It brings cost savings as well as some architectural benefit. Due to its minimalistic philosophy, Go is suitable not only for applications that leverage sophisticated concurrency, but also for simple operations as the one described in this post. This article discusses setup, authentication, using Windows and leveraging environment variables. It also shows how to do JWT generation and integration with API Gateway.

Introduction

When I was playing around with my pet-project Kyiv Station Walk, I noticed that manually removing test data is tedious and I need to come up with a concept of the admin page. This required some sort of authentication endpoint. Some super-lightweight service which would check login and password against as a pair of super-user credentials.

Serverless is quite useful for this simple nanoservice. This brings some cost-saving as serverless comes to me almost free due to low execution rate that I anticipate for the admin page of my low-popular service. Also, I would argue that this brings me some architectural benefit because it allows me to split my core domain from cross-cutting concern. For my task, I’ve decided to use AWS Lambda. I’ve also decided to use Go as it is due to its minimalistic nature which would be useful for Lambda instantiation.

Setup

Our lambda function to be called from outside over HTTP, so we place HTTP Gateway in front of it so it would look something like below in AWS Console.

Image 1

Project Structure

In order to decouple our authentication logic from FaaS internals, our project will have two files: auth.go is where the authentication logic will reside and main.go where our logic is integrated with AWS lambda.

Contents of main.go will look as follows:

Go
func clientError(status int) (events.APIGatewayProxyResponse, error) {
    return events.APIGatewayProxyResponse{
        StatusCode: status,
        Body:       http.StatusText(status),
    }, nil
}

func HandleRequest(req events.APIGatewayProxyRequest) 
    (events.APIGatewayProxyResponse, error) {
    jwtToken, err := Auth(req.Body)

    if err != nil {
        return clientError(http.StatusForbidden)
    }

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

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

For this code to work, we’ll need "github.com/aws/aws-lambda-go/lambda" package.

In order for our endpoint to be consumed from the outside because we have to provide the response in a special format for the API gateway. For this reason, we’ve installed github.com/aws/aws-lambda-go/events package as well.

Let’s highlight the example of a successful response you may have noticed in the snippet above:

Go
events.APIGatewayProxyResponse{
    StatusCode: http.StatusOK,
    Body:       jwtToken,
}

Error response looks as below:

Go
events.APIGatewayProxyResponse{
    StatusCode: status,
    Body:       http.StatusText(status),
}

Authentication

For our purposes, we’ll omit the usage of persistent storage since one pair of credentials is enough. Still, we need to hash stored password in with the hash function which will allow for the defender to verify password in acceptable time but will require for attacker a lot of resources to guess a password from the hash. Argon2 is recommended for such a task. So to start off, we’ll need "github.com/aws/aws-lambda-go/lambda" package.

JavaScript
func main() {
    lambda.Start(HandleRequest)
}

Argon2 is implemented in "golang.org/x/crypto/argon2" so the authentication is quite straightforward.

JavaScript
func HandleRequest(ctx context.Context, credentials Credentials) (string, error) {
    password := []byte{221, 35, 76, 136, 29, 114, 39, 75, 41, 248, 62, 216, 149, 39, 
    248, 154, 243, 203, 188, 106, 206, 74, 122, 47, 255, 61, 173, 43, 102, 173, 222, 125}

    if credentials.Login != login {
        return "auth failed", errors.New("auth failed")
    }
    key := argon2.Key([]byte(credentials.Password), []byte(salt), 3, 128, 1, 32)
    if areSlicesEqual(key, password) {
        return "ok", nil
    }
    return "auth failed", errors.New("auth failed")
}

Note how for both wrong login and incorrect password, we’re returning the same message in order to disclose as little information as possible. This allows us to prevent account enumeration attack.

Building it:

go build -o main main.go
And zipping it
~\Go\Bin\build-lambda-zip.exe -o main.zip main

Using Windows

If you’re a Windows user, you’ll need the following environment variables set before building:

Image 2

Leveraging Environment Variables

We can see our credentials hardcoded in a codebase for now. This is poor practice because they are subject to automatic harvesting of credentials.

You can leverage environment variables instead with the help of os package.

JavaScript
login := os.Getenv("LOGIN")
salt := os.Getenv("SALT")

Here’s how you set up them in AWS console.

Image 3

JWT Generation

Once the service verifies that credentials are valid, it issues a token which allows its bearer to act as a super-user. For this purpose, we’ll use JWT which is a de-facto standard format for access tokens.

We’ll need the following package:

"github.com/dgrijalva/jwt-go"

The JWT generation code looks as follows:

JavaScript
type Claims struct {
    Username string `json:"username"`
    jwt.StandardClaims
}

func issueJwtToken(login string) (string, error) {
    jwtKey := []byte(os.Getenv("JWTKEY"))

    expirationTime := time.Now().Add(1 * time.Hour)
    claims := &Claims{
        Username: login,
        StandardClaims: jwt.StandardClaims{
            // In JWT, the expiry time is expressed as unix milliseconds
            ExpiresAt: expirationTime.Unix(),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

Since an adversary who intercepts such token may act on behalf of super-user, we don’t want this token to be effective infinitely because this will grant adversary infinite privileges. So we set token expiration time for one hour.

Testing API Gateway

At this point, our API is ready to be consumed. Here’s a brief snippet from the main service that deletes a route only if the user has sufficient rights.

F#
let delete (id: string) =
    fun (next: HttpFunc) (httpContext : HttpContext) ->
    let result =
        AuthApi.authorize httpContext
        |> Result.bind (fun _ -> ElasticAdapter.deleteRoute id)
    match result with
    | Ok _ -> text "" next httpContext
    | Error "ItemNotFound" -> RequestErrors.BAD_REQUEST "" next httpContext
    | Error "Forbidden" -> RequestErrors.FORBIDDEN "" next httpContext
    | Error _ -> ServerErrors.INTERNAL_ERROR "" next httpContext

let authorize (httpContext : HttpContext) =
    let authorizationHeader = httpContext.GetRequestHeader "Authorization"
    let authorizationResult =
        authorizationHeader
        |> Result.bind JwtValidator.validateToken
    authorizationResult

let validateToken (token: string) =
    try
        let tokenHandler = JwtSecurityTokenHandler()
        let validationParameters = createValidationParameters
        let mutable resToken : SecurityToken = null
        tokenHandler.ValidateToken(token, validationParameters, &resToken)
        |> ignore
        Result.Ok()
    with
    | _ -> Result.Error "Forbidden"

Minimizing Attack Surface

At this point, our function is open to some vulnerabilities so we have to perform some additional work on our API gateway.

Endpoint Throttling

The default settings are too high for authorization function that is not expected to be invoked often. Let’s change this.

Image 4

IP Whitelist

Neither do we want our function to be accessible from any IP possible. The following snippet in the "Resource policy" API gateway settings section allows us to create a whitelist of IP addresses that can access our lambda.

Image 5

In order to obtain ARN, we can navigate back to Lambda configuration page and check it by clicking on API Gateway icon.

Image 6

Conclusion

Serverless is a great option for smallish nanoservices. Due to its minimalistic philosophy, Go is suitable not only for applications that leverage sophisticated concurrency but also for such simple operations as the one that is described above.

History

  • 19th February, 2020: Initial version
  • 3rd August, 2022: Added notes on decoupling business logic from FaaS internals

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)