Contents

Kaboo #1 - Golang - gorilla/mux + Auth0

Introduction

This is the first post from a series that’ll focus on my experience with Golang, building the backend of an online card-game developed for learning purposes. The game is called Kaboo, someone was nice enough to post a gist summarizing the game rules. This post will focus on the API design and implementation, using a combination of REST and WebSocket (WS for “real-time” game play).

There are numerous tutorials and articles out there, most of them are focus on very basic sample-apps (e.g. the classic todo-app). Instead, I will focus on the design and implementation details of a slightly more complex application. Hopefully it’ll give a more complete introduction to some concepts in Golang.

All the code is publicly available here - The tag post-1 contains the tree exactly as it was when I wrote this post, you can download it and run the server.

Work in Progress
The game is a work-in progress. I'm working on it and releasing these posts adjacent to coding, this means that the code you'll see right now is incomplete and that the posts content might change in the future.

API Design

The first thing I always suggest doing before head-diving into VSCode is to plan, this is not an easy task, and usually not as fun as coding. On the long-run it can save a lot of time, and your coding velocity will be much higher.

I divided the game into several high-level components, at first the division is only “theoretical” to help get a better view of what kind of work will be needed. Later when we start to implement, the actual component-separation can change and we can make use of concepts such as modules and go-packages. Overall we can divide the game into the following parts:

  • transport
    • HTTP Server serving the REST and WebSocket APIs, authentication will be done on this level, ideally the code will be modular enough so we can easily change the API, add another transport mechanism (did someone say QUIC?)
  • backend
    • Actual business logic will sit here, actions such as creating a new game, joining an existing game or making game actions… etc’
    • gameengine
      • The game logic (rules, changing a running game state… etc’), for now we will not focus much on modularity but maybe in the future we’ll see how easy or hard it is to switch the game-engine to a different one.
  • models
    • Anything DB related, saving and reading objects from the db, querying it

This post will focus on the transport component (or more accurately, go package).

Implementation

Using gorilla/mux to route and net/http to serve

We will use gorilla/mux which is a really nice HTTP router and URL matcher, first let’s define our Server and API structs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Server struct {
  // The auth middleware we will use to authenticate the JWT token provided by Auth0
  authMiddleware JWTAuthMiddleware
  // The API implementation
  api            API
  // Our WebSocket "hub", where HTTP connection upgrade happens and we register the connected clients
  hub            *websocket.Hub
  // The port the server will listen on
  restPort       int
}

type API struct {
  // Our GameController backend instance, this is how the API <> Backend wiring happens
  gameController *backend.GameController
}

We can then implement our NewServer(...) and Start() methods, which instantiate and starts the server. (We also create out database instance but we’ll talk about it in a later post)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package transport

import (
  "github.com/gorilla/handlers"
  "github.com/gorilla/mux"
  // ... trimmed
)

const (
  apiVersion = "1"
)

func NewServer(restPort int, auth0Domain string, auth0Audience string) Server {
  var db models.Db
  db.Open("mongodb://localhost:27017/", "kaboo")
  hub := websocket.NewHub()
  go hub.Run()
  return Server{
    JWTAuthMiddleware{
      &db,
      auth0Domain,
      auth0Audience,
    },
    API{
      gameController: backend.NewGameController(&db, hub),
    },
    hub,
    restPort,
  }
}

// Start starts the server
func (s *Server) Start() {
  r := mux.NewRouter()
  if os.Getenv("DEBUG") != "" {
    r.Use(handlers.CORS(
      handlers.AllowedOrigins([]string{"*"}),
      handlers.AllowedHeaders([]string{"Content-Type", "Authorization"}),
      handlers.AllowCredentials(),
    ))
  }
  apiRouter := r.PathPrefix(fmt.Sprintf("/api/v%s", apiVersion)).Subrouter()
  apiRouter.HandleFunc("/game/new", s.authMiddleware.Handle(s.api.handleNewGame))
  apiRouter.HandleFunc("/game/join", s.authMiddleware.Handle(s.api.handleJoinGame))
  apiRouter.HandleFunc("/game/leave", s.authMiddleware.Handle(s.api.handleLeaveGame))

  apiRouter.HandleFunc("/state", s.authMiddleware.Handle(notImplemented))
  apiRouter.HandleFunc("/ws", s.authMiddleware.Handle(s.hub.HandleWSUpgradeRequest))

  log.Infof("Starting API server (:%v)\n", s.restPort)
  http.ListenAndServe(fmt.Sprintf(":%d", s.restPort), handlers.CombinedLoggingHandler(log.StandardLogger().Out, r))
}

func notImplemented(w http.ResponseWriter, r *http.Request, user *models.User) {
  http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

// ... API handlers ...

The Start() method is pretty lean, we begin by enabling a very loose CORS policy using gorilla CORS middleware (gorilla/handlers) if the server is running in DEBUG mode - this will allow the test client to make REST requests to the API from a different origin. Immediately followed is the router instantiation, with several API methods. We use a custom HTTP handler to validate the request authentication (using JWT tokens, we will dive into Auth0 in a bit). And route the special /ws path to our WebSocket “hub”, the component handling websocket connections.

Finally using http.ListenAndServe(..) to serve our API with a handlers.CombinedLoggingHandler to log requests to the standard output.

Handling incoming requests

A request life-cycle goes through several phases, I’ve provided handleNewGame() below and some helper methods - we start by trying to decode the incoming request JSON. Go has some pretty powerful json encoding and decoding utilities that can help us. The JSON requests and responses (and the helper method decodeJSONBody()) are sitting in models.go. We use encoding/json to try and decode the request body and map it to a struct, there are numerous errors that can happen in the decoding process (wrong content type, bad json, request EOF… etc’) and it’s important to handle them in a production system, undefined behavior and unhandled errors are evil - luckily for us Go is very explicit about error handling.

After decoding the request we call the backend and send the response as JSON. The decoupling between the API and the backend is important for easier refactoring, it’ll also make writing tests easier as we’ll see soon.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// ....

func (a *API) handleNewGame(w http.ResponseWriter, r *http.Request, user *models.User) {
  var req createGameReq
  if tryToDecodeOrFail(w, r, &req) != nil {
    return
  }
  gameID, err := a.gameController.NewGame(user, req.Name, req.MaxPlayersCount, req.Password)
  if err != nil {
    http.Error(w, err.Error(), 500)
    return
  }
  tryToWriteJSONResponse(w, r, &createGameRes{GameID: gameID})
}


func tryToDecodeOrFail(w http.ResponseWriter, r *http.Request, dst interface{}) error {
  if err := decodeJSONBody(w, r, dst); err != nil {
    var mr *malformedRequest
    if errors.As(err, &mr) {
      http.Error(w, mr.msg, mr.status)
    } else {
      log.Println(err.Error())
      http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
    }
    return err
  }
  return nil
}

func tryToWriteJSONResponse(w http.ResponseWriter, r *http.Request, res interface{}) error {
  jsonRes, err := json.Marshal(res)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return err
  }
  w.Write([]byte(jsonRes))
  return nil
}

// ... part of models.go

type createGameReq struct {
  Name            string `json:"name"`
  MaxPlayersCount int    `json:"maxPlayers"`
  Password        string `json:"password"`
}

type createGameRes struct {
  GameID string `json:"id"`
}

func decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
  if r.Header.Get("Content-Type") != "" {
    value, _ := header.ParseValueAndParams(r.Header, "Content-Type")
    if value != "application/json" {
      msg := "Content-Type header is not application/json"
      return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: msg}
    }
  }

  r.Body = http.MaxBytesReader(w, r.Body, 1048576)

  dec := json.NewDecoder(r.Body)
  dec.DisallowUnknownFields()

  err := dec.Decode(&dst)
  if err != nil {
    var syntaxError *json.SyntaxError
    var unmarshalTypeError *json.UnmarshalTypeError

    switch {
    case errors.As(err, &syntaxError):
      msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
      return &malformedRequest{status: http.StatusBadRequest, msg: msg}
      // ... Trimmed most of the switch cases
    default:
      return err
    }
  }

  if dec.More() {
    msg := "Request body must only contain a single JSON object"
    return &malformedRequest{status: http.StatusBadRequest, msg: msg}
  }

  return nil
}

Auth0 + JWT authentication

As previously noted, API access is authenticated using JWT. JWT (JSON Web Token) is an open standard for specifying and validating security claims (entity X has access to resource Y for the next Z minutes, for example). There are numerous possible use-cases for JWT, our authentication flow will be -

  1. The user authenticates against Auth0 using their very nice authentication library, upon successful authentication a JWT token is generated and sent to the client.
  2. The JWT token contains base64-encoded information about the user, such as the user id and token expiration time. The token is signed using a private key (a secret generated by Auth0) we can later validate, and the signature is added to the token.
  3. API requests to our server will contain the token in the Authorization HTTP header
  4. On incoming requests the server will extract the token and validate the signature and claims (expiration time of the token for example, if the user is banned… etc’)
  5. On successful validation the handler will call the next one, on failure the handler will return an error and ignore the next handler.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// transport/jwtauth.go
package transport

import (
  "fmt"
  "net/http"
  "os"
  "regexp"

  log "github.com/sirupsen/logrus"

  "github.com/auth0-community/go-auth0"
  "github.com/ngutman/kaboo-server-go/models"
  "gopkg.in/square/go-jose.v2"
  "gopkg.in/square/go-jose.v2/jwt"
)

// JWTAuthMiddleware handles Auth0 JWT validation
type JWTAuthMiddleware struct {
  db            *models.Db
  auth0Domain   string
  auth0Audience string
}

// Handle implements the JWT validation over incoming request
func (j *JWTAuthMiddleware) Handle(next func(w http.ResponseWriter, r *http.Request, user *models.User)) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
    // For faster development allowed to skip token authentication in DEBUG mode
    if os.Getenv("DEBUG") != "" {
      debugToken := regexp.MustCompile(`DebugToken (.*)`).FindStringSubmatch(r.Header.Get("Authorization"))
      if len(debugToken) > 1 {
        log.Debugf("Debug token, setting user to %v\n", debugToken[1])
        user, _ := j.db.UserDAO.FetchUserByExternalID(debugToken[1])
        next(w, r, user)
        return
      }
    }
    client := auth0.NewJWKClient(auth0.JWKClientOptions{URI: fmt.Sprintf("https://%s/.well-known/jwks.json", j.auth0Domain)}, nil)
    audience := j.auth0Audience
    configuration := auth0.NewConfiguration(client, []string{audience}, fmt.Sprintf("https://%s/", j.auth0Domain), jose.RS256)
    validator := auth0.NewValidator(configuration, nil)

    token, err := validator.ValidateRequest(r)
    if err != nil {
      log.Errorf("Token %v is invalid, %v\n", token, err)
      http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
    } else {
      claims := jwt.Claims{}
      validator.Claims(r, token, &claims)
      // Attach user to request context
      user, _ := j.db.UserDAO.FetchUserByExternalID(claims.Subject)
      next(w, r, user)
    }
  }
}

The above code implements JWT authentication using Auth0 Go libraries, first we allow skipping the authentication all together if the server runs in DEBUG mode, this will save us time generating JWT tokens. If the server runs in production mode, we generate a new JWT client which receives the URL that contains the public part of the key that was used to sign the token. The public-key is cached in-memory, Auth0 library will verify the token signature and expiration date, if successful we will fetch the user from the db and call the next method handler.

Authentication
Authentication is one of the most critical parts of a public system (no duh), the code provided here is for learning purposes so make sure you fully understand how to safely authenticate your server!

What’s next?

At this point we have a working HTTP server listening for incoming authenticated API calls! yay. You can download and play with the repository, the server can run in DEBUG mode to test without Auth0.

Next on the series we will dive deep into the websocket implementation (for real time actions and messaging), database related code and the actual game engine.