Learn go series: Part III - Stuck in the middle!

By Gaurav on 9 Jan 2018

Alternate title: Working with JWT, CORS as middlewares using Negroni.

This is Part 3 of "Learn go" series. You can find the previous post here.

In this post, I will be walking you through an example of adding middlewares for working with JWT for sessions. As well as making an API, CORS compatible. This post picks up from the previous posts Part I and, Part II, so if you haven't read, please skim through it.

Let's begin…

Problem Statement

We need an API where when we login, we get a JWT token back for authentication, similar in the manner to session cookies.

Creating the Login API

We are creating a login handler. Let's call it CreateSessionHandler

api/user.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type credentials struct {
  Username string
  Password string
}

func CreateSessionHandler(w http.ResponseWriter, req *http.Request) {
  var creds credentials
  json.NewDecoder(req.Body).Decode(&creds)

  var user model.User
  db.Instance().Where("username = ?", creds.Username).First(&user)

  if model.ComparePasswords(user.HashedPassword, creds.Password) {
    tokenMap := model.CreateJWTToken(user, jwtSigningKey)

    json.NewEncoder(w).Encode(tokenMap)
  } else {
    http.Error(w, "Not Authorized", unauthorized)
    return
  }
}

How does the ComparePasswords and CreateJWTToken actually work?

models/user.go

1
2
3
4
5
6
7
8
9
10
func ComparePasswords(hashedPwd string, plainPwd string) bool {
  byteHash := []byte(hashedPwd)
  err := bcrypt.CompareHashAndPassword(byteHash, []byte(plainPwd))
  if err != nil {
    log.Println(err)
    return false
  }

  return true
}

For comparing passwords, we use [bcrypt][bcypt]'s CompareHashAndPassword function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func CreateJWTToken(user User, jwtSigningKey []byte) map[string]string {
  token := jwt.New(jwt.SigningMethodHS256)

  /* Create a map to store our claims */
  claims := token.Claims.(jwt.MapClaims)

  /* Set token claims */
  claims["user"] = user
  claims["userID"] = user.ID

  /* Sign the token with our secret */
  tokenString, _ := token.SignedString(jwtSigningKey)

  tokenMap := map[string]string{"token": tokenString}

  return tokenMap
}

For creating a jwtToken we need a jwtSigningKey defined. This is a random alphanumeric string. This needs to be kept secret as this verifies the signed token's authenticity. We can then create a new token using jwt.

Validating and authenticating using JWT Token

We aren't done yet. We have created the token, now we need a way to verify and validate the JWT token in requests for other APIs.

Let's first write the middleware…

api.go

1
2
3
4
5
6
7

  jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      return jwtSigningKey, nil
    },
    SigningMethod: jwt.SigningMethodHS256,
  })

Note: This is a third-party middleware.

We can hook this up in multiple ways to our mux router. Let me use a middleware library called negroni. Rewriting the example from the Part I, as

api.go

1
2
3
4
  router.HandleFunc("/hello", negroni.New(
    negroni.HandlerFunc(jwtMiddleware.HandlerWithNext),
    negroni.Wrap(helloHandler),
  )).Methods("GET")

We are creating a new Negroni instance and passing it the jwtMiddleware's HandlerWithNext function and the wrapped helloHandler function.

CORS

We are almost done. Our API is almost functional. We run it, test it and it works fine. Then we hit the API from the UI on a different domain, and BAM it doesn't work. Bummer!

Well, we forgot to enable CORS support to our server. Duh!

Let's do this by using a simple CORS package.

api.go

1
2
  handler := cors.Default().Handler(router)
  http.ListenAndServe(":"+8080, handler)

We are wrapping our router with the default cors' Handler. We open up our browser and test again. And voila! Things work like a charm.

Caveats & Honorable mentions

  • JWT as Sessions - Counter Argument against JWT cause JWT aren't session cookies

References

Signing off for now. As always, please leave your thoughts and comments in the section below.

comments powered by Disqus