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.