How to deploy a Go app on AWS Fargate – part 2

Photo by Adam Cohn

We are going to secure our Go Microservice with JWT (JSON Web Tokens). We start with a fairly simple Go file, located in pkg/auth/authenticate.go, importing a couple of packages.

The most important one is, of course, jwt, because it’ll help us implementing the authentication.

Authenticating the user

First we need to generate the token, of course:

package auth

import (
    "log"
    "os"
    "time"

    jwt "github.com/dgrijalva/jwt-go"
)

func GenerateJWT() (string, error) {

    signingKey := []byte(os.Getenv("SECRET_KEY"))

    payload := jwt.New(jwt.SigningMethodHS256)

    claims := payload.Claims.(jwt.MapClaims)

    claims["authorized"] = true
    claims["exp"] = time.Now().Add(time.Minute * 1).Unix()

    token, err := payload.SignedString(signingKey)

    if err != nil {
        log.Printf("Could not sign the payload: %s", err.Error())
        return "", err
    }

    return token, nil
}

In the first line of the method we get the signing key that will be used to generate the token. Then we declare that the signing algorithm will be HS256, one of the simplest.

In the next few lines we declare a map of fields (claims) that will be the payload of our token. In this case we only declare the expiration (exp: in our example the JWT will not be accepted after one minute).

Finally we sign the payload with the signing key and return it.

To test the logic, let’s add a token route in the main function. We also create a Token struct so we return a proper JSON:

package main

import (
    "log"
    "net/http"

    "github.com/g-dem/go-fargate-demo/pkg/auth"
    "github.com/g-dem/go-fargate-demo/pkg/handler"
    "github.com/g-dem/go-fargate-demo/pkg/listing"
    _ "github.com/go-sql-driver/mysql"
    "github.com/julienschmidt/httprouter"
)

func main() {
    router := httprouter.New()

    router.GET("/token", getToken())
    router.GET("/companies", getCompanies())

    log.Fatal(http.ListenAndServe(":8080", router))
}

func getCompanies() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    return handler.Writer(listing.GetAllCompanies())
}

type Token struct {
    Token string `json:"token"`
}

func getToken() func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    var t Token

    validToken, err := auth.GenerateJWT()

    t.Token = validToken
    log.Println(validToken)
    if err != nil {
        log.Println("Failed to generate token")
    }

    return handler.Writer(t)
}

Cool….

…but not very useful. Of course we want our user to provide valid username and password before issuing a token. After undoing the changes in main.go, we add these three new struct in authenticate.go

// ...

type Token struct {
    Token string `json:"token"`
}

type User struct {
    Username string `json:"username"`
    Password uint64 `json:"password"`
}

type Exception struct {
    Message string `json:"message"`
}

Next we need a method that does three things:

  1. checks that the request has the correct payload (a User);
  2. checks that username and password are valid – to keep it simple, we will check against some values inside the .env file
  3. returns some error or a token.

(Actually #2 will be in a separate method called validate).

Here is the complete authenticate.go.

package auth

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

    jwt "github.com/dgrijalva/jwt-go"
    "github.com/joho/godotenv"
    "github.com/julienschmidt/httprouter"
)

type Token struct {
    Token string `json:"token"`
}

type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type Exception struct {
    Message string `json:"message"`
}

func validate(u User) bool {
    username := os.Getenv("TEST_USERNAME")
    password := os.Getenv("TEST_PASSWORD")
    if u.Username == username && u.Password == password {
        return true
    }
    return false
}

func Authenticate(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
    var u User
    err := json.NewDecoder(req.Body).Decode(&u)
    if err != nil {
        json.NewEncoder(w).Encode(Exception{Message: err.Error()})
        return
    }

    if !validate(u) {
        json.NewEncoder(w).Encode(Exception{Message: "invalid credentials"})
        return
    }

    t, err := generateJWT(u)
    if err != nil {
        json.NewEncoder(w).Encode(Exception{Message: "Error while generating the token"})
        return
    }

    json.NewEncoder(w).Encode(Token{Token: t})
}

func generateJWT(u User) (string, error) {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    signingKey := []byte(os.Getenv("SECRET_KEY"))

    payload := jwt.New(jwt.SigningMethodHS256)

    claims := payload.Claims.(jwt.MapClaims)

    claims["authorized"] = true
    claims["user"] = u.Username
    claims["exp"] = time.Now().Add(time.Minute * 1).Unix()

    token, err := payload.SignedString(signingKey)

    if err != nil {
        log.Printf("Could not sign the payload: %s", err.Error())
        return "", err
    }

    return token, nil
}

Authorizing requests

Now we need to put JWT at work, ie use it to authorize requests. To do this, we are going to wrap the protected routes (/companies in our app) with a authorize middleware.

This middleware (that we are going to put in pkg/auth/authorize.go ) will parse the request’s header for Bearer authorization token and check it against our secret key. If everything goes smooth (the token is valid), we pass the request to next.

func AuthorizeMiddleware(next httprouter.Handle) httprouter.Handle {
    return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
        w.Header().Set("Content-Type", "application/json")
        authorizationHeader := req.Header.Get("Authorization")
        if authorizationHeader == "" {
            json.NewEncoder(w).Encode(Exception{Message: "An Authorization header is required"})
            return
        }
        bearerToken := strings.Split(authorizationHeader, " ")
        if len(bearerToken) == 2 {
            token, err := parseBearerToken(bearerToken[1])
            if err != nil {
                json.NewEncoder(w).Encode(Exception{Message: err.Error()})
                return
            }
            if token.Valid {
                context.Set(req, "decoded", token.Claims)
                next(w, req, ps)
                return
            }
            json.NewEncoder(w).Encode(Exception{Message: "Invalid Authorization token"})
            return
        }
    }
}



func parseBearerToken(bearerToken string) (*jwt.Token, error) {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    secretKey := os.Getenv("SECRET_KEY")
    return jwt.Parse(bearerToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("there was an error")
        }
        return []byte(secretKey), nil
    })
}

First we check that the request has an authorization header. Next, since the Bearer token comes in this form Bearer <THE_TOKEN>, we split the Authorization header in two strings and get the second, our token. Finally we pass the token to the parseBearerToken method and return either next() or an error.

The parseBearerToken method retrieves the SECRET_KEY from .env and uses jwt.Parse to check if the token is valid.

In our main.go we wrap getCompanies with AuthorizeMiddleware and we are ready to test.

// main.go

// ...
router.GET("/companies", auth.AuthorizeMiddleware(getCompanies()))

// ...

Of course if we curl http://localhost:8080/companies we get an error:

$ curl http://localhost:8080/companies
{"message":"An Authorization header is required"}

What if we have a token, but we are too slow and it expires?

$ curl -H 'Accept: application/json' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5c[CUT]
OUh_esOHS8611mrKqdTzA" http://localhost:8080/companies   
{"message":"Token is expired"}

And what if the token is invalid because I changed a letter of a valid one?

$ curl -H 'Accept: application/json' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5c[CUT]
_kwWldAp4x9UlLBJSuEXj" http://localhost:8080/companies  
{"message":"signature is invalid"}

Finally:

$ curl -H 'Accept: application/json' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5c[CUT]" http://localhost:8080/companies 
[{"id":1,"name":"Acme Inc."},{"id":2,"name":"Gekko \u0026 Co."},{"id":3,"name":"Duff Beer"}]

And we are ready to go to production.