Building Auth Endpoint with Go and AWS Lambda

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 that would check login and password against a pair of super-user credentials.
Serverless is quite useful for this simple nano service. This brings some cost-saving as serverless comes to me almost free due to the 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, due to its minimalistic nature which would be useful for Lambda instantiation.
 

Setup

 
Building Auth Endpoint With Go And AWS Lambda
 
Our lambda function is to be called from outside over HTTP, so we place the HTTP Gateway in front of it so it would look something like below in the AWS Console.
 

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  a lot of resources for an attacker to guess a password from the hash. Argon2 is recommended for such a task. So to start off, we’ll need the "github.com/aws/aws-lambda-go/lambda" package.
  1. func main() { 
  2.     lambda.Start(HandleRequest) 
  3. }  
Argon2 is implemented in "golang.org/x/crypto/argon2", so the authentication is quite straightforward.
  1. func HandleRequest(ctx context.Context, credentials Credentials) (string, error) { 
  2.     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}  
  3.   
  4.     if credentials.Login != login { 
  5.         return "auth failed", errors.New("auth failed") 
  6.     }  
  7.     key := argon2.Key([]byte(credentials.Password), []byte(salt), 3128132)  
  8.     if areSlicesEqual(key, password) { 
  9.         return "ok", nil 
  10.     }  
  11.     return "auth failed", errors.New("auth failed")  
  12. }  
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 an account enumeration attack.
 
Building it:
  1. go build -o main main.go  
  2. And zipping it  
  3. ~\Go\Bin\build-lambda-zip.exe -o main.zip main  

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 an os package.
  1. login := os.Getenv("LOGIN")  
  2. salt := os.Getenv("SALT")  
Here’s how you set them up in an AWS console:
Building Auth Endpoint With Go And AWS Lambda

JWT Generation

 
Once the service verifies that credentials are valid, it issues a token that allows it’s 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:
  1. "github.com/dgrijalva/jwt-go"  
The JWT generation code looks like the following:
  1. type Claims struct { 
  2.     Username string `json:"username"` 
  3.     jwt.StandardClaims 
  4. }  
  5.   
  6. func issueJwtToken(login string) (string, error) { 
  7.     jwtKey := []byte(os.Getenv("JWTKEY")) 
  8.  
  9.     expirationTime := time.Now().Add(1 * time.Hour) 
  10.     claims := &Claims{ 
  11.         Username: login, 
  12.         StandardClaims: jwt.StandardClaims{ 
  13.             // In JWT, the expiry time is expressed as unix milliseconds 
  14.             ExpiresAt: expirationTime.Unix(), 
  15.         },  
  16.     }  
  17.     token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)  
  18.     return token.SignedString(jwtKey)  
  19. }  
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 the token expiration time for one hour.
 

Integration with API gateway

 
It turns out that our endpoint is not yet ready to be consumed from the outside, because we have to provide the response of a special format for API gateway. To fix this, let's install github.com/aws/aws-lambda-go/events package.
 
Here’s an example of a successful response:
  1. events.APIGatewayProxyResponse{ 
  2.     StatusCode: http.StatusOK, 
  3.     Body:       jwtToken, 
  4. }  
Now 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.
  1. let delete (id: string) =  
  2.     fun (next: HttpFunc) (httpContext : HttpContext) ->  
  3.     let result =  
  4.         AuthApi.authorize httpContext  
  5.         |> Result.bind (fun _ -> ElasticAdapter.deleteRoute id)  
  6.     match result with  
  7.     | Ok _ -> text "" next httpContext  
  8.     | Error "ItemNotFound" -> RequestErrors.BAD_REQUEST "" next httpContext  
  9.     | Error "Forbidden" -> RequestErrors.FORBIDDEN "" next httpContext  
  10.     | Error _ -> ServerErrors.INTERNAL_ERROR "" next httpContext  
  11.   
  12. let authorize (httpContext : HttpContext) =  
  13.     let authorizationHeader = httpContext.GetRequestHeader "Authorization"  
  14.     let authorizationResult =  
  15.         authorizationHeader  
  16.         |> Result.bind JwtValidator.validateToken  
  17.     authorizationResult  
  18.   
  19. let validateToken (token: string) =  
  20.     try  
  21.         let tokenHandler = JwtSecurityTokenHandler()  
  22.         let validationParameters = createValidationParameters  
  23.         let mutable resToken : SecurityToken = null  
  24.         tokenHandler.ValidateToken(token, validationParameters, &resToken)  
  25.         |> ignore  
  26.         Result.Ok()  
  27.     with  
  28.     | _ -> 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.
 
Building Auth Endpoint With Go And AWS Lambda
 

IP whitelist

 
We also don't 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. 
 
Building Auth Endpoint With Go And AWS Lambda
 
In order to obtain ARN, we can navigate back to the Lambda configuration page and check it by clicking on the API Gateway icon. 
 
Building Auth Endpoint With Go And AWS Lambda
 

Conclusion

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