Thoughts of a software developer

21.07.2018 20:02 | Modified 11.08. 18:29
Simple Golang JWT authorize

I made a small test with JSON Web Tokens to understand it better. Haven’t used it before and I was interested to see how it works and what it offers. Link to the project in Github

Julien Schmidt’s router was the one I chose for the job. Also jwt-go library is essential.

func main() {
    router := httprouter.New()
    router.RedirectFixedPath = true
    router.RedirectTrailingSlash = true
    router.POST("/authenticate", authenticate)
    router.GET("/protected", authorizeMiddleware(http.HandlerFunc(protectedEndpoint)))
    log.Fatal(http.ListenAndServe(":8700", router))
}

We have two essential endpoints /authenticate and /protected. Authenticate endpoint is the one where you POST your credentials and as an answer, you get a token which is supposed to be used for the GET query to /protected endpoint.

There is no web page for delivering the credentials, but the functionality can be tested with

curl -X POST -d '{"username":"test-username","password":"test-password"}' localhost:8700/authenticate

and you get an answer like:

{"token":"eyJhbGciOiJIUzM4NCI..."}

Authentication

POST request body (credentials) is transformed into a User struct and checked for equality to hard coded credentials (validateCredentials function).

func authenticate(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
    var user User
    err := json.NewDecoder(req.Body).Decode(&user)
    if err != nil {
        json.NewEncoder(w).Encode(Exception{Message: err.Error()})
        return
    }
    if validateCredentials(user) {
        json.NewEncoder(w).Encode(JwtToken{Token: signedTokenString(user)})
        return
    }
    json.NewEncoder(w).Encode(Exception{Message: "invalid credentials"})
}
func validateCredentials(user User) bool {
    if user.Username == username && user.Password == password {
        return true
    }
    return false
}
func signedTokenString(user User) string {
    token := jwt.NewWithClaims(jwt.SigningMethodHS384, jwt.MapClaims{
        "username": user.Username,
        "password": user.Password,
    })
    tokenString, err := token.SignedString([]byte(secretKey))
    if err != nil {
        log.Println(err)
    }
    return tokenString
}

If the credentials are a match to the hard coded one’s, the user object or struct is signed with HMAC-SHA-384 and a secret key (hard coded constant in this case).

Authorization

Authorization is done with a middleware so that it can be used easily with many different endpoints.

Now, with the token got from the /authenticate endpoint, we can make a request to /protected endpoint with a Authorization header.

curl -H 'Authorization:Bearer eyJhbGciOiJIUzM4NCI...' "localhost:8700/protected"
func authorizeMiddleware(next http.HandlerFunc) httprouter.Handle {
    return httprouter.Handle(func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
        authorizationHeader := req.Header.Get("Authorization")
        if authorizationHeader != "" {
            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)
                    return
                }

If token is valid, we set the decoded content into request and continue to the function which returns the content for the actual endpoint requested.

                json.NewEncoder(w).Encode(Exception{Message: "Invalid Authorization token"})
                return
            }
        }
        json.NewEncoder(w).Encode(Exception{Message: "An Authorization header is required"})
    })
}

parseBearerToken function checks that the signing method is what we signed the content with.

func parseBearerToken(bearerToken string) (*jwt.Token, error) {
    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
    })
}

The protected content is the username and password in JSON in this case. The JSON Web Token got from the request.

func protectedEndpoint(w http.ResponseWriter, req *http.Request) {
    decoded := context.Get(req, "decoded")
    var user User
    mapstructure.Decode(decoded.(jwt.MapClaims), &user)
    json.NewEncoder(w).Encode(user)
}

The answer from /protected endpoint:

{"username":"test-username","password":"test-password"}

Next steps

This is only a simple example. Using JSON Web Tokens safely means taking into consideration different kind of attacks (XSS for example). The requests should be secured with a SSL sertificate (like always anyway). If I have time, I might make a React page to explore this a bit.