Building auth endpoint with Go and AWS Lambda
Bohdan Stupak
Posted on February 26, 2020
Originally published at my blog
When I was playing around with my pet-project Kyiv Station Walk I’ve 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 due to its minmalistic 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.
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.
func main() {
lambda.Start(HandleRequest)
}
Argon2 is implemented in "golang.org/x/crypto/argon2"
So the authentication is quite straightforward
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
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
login := os.Getenv("LOGIN")
salt := os.Getenv("SALT")
Here’s how you set up them in AWS console
JWT Generation
Once the service verifies that credentials are valid it issues a token which 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
"github.com/dgrijalva/jwt-go"
The JWT generation code looks as follows
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.
Integration with API gateway
Turns out that our endpoint is yet not ready to be consumed from the outside because we have to provide the response of a special format for API gateway. To fix this lets install github.com/aws/aws-lambda-go/events
package.
Here’s the example of a successful response
events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: jwtToken,
}
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.
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"
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.
Posted on February 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.