Gorilla router: the very basic usage

I’ve been using gorilla mux for quite some time. It has pretty much become the de facto as it is convenient and easy to use.

Say I am making a web app for organizing events, where it needs to have:

  • A public web interface for users
  • A private web interface for authenticated admins
    • Uses secure cookies
  • An API with authentication
    • Uses an HTTP header

To start with, my new router kind of looks like this:

// My new router <3
router := mux.NewRouter()
// Serves the user a public html page
router.HandleFunc("/", handleIndex).Methods("GET")
// Handles a POST request to login, which sets an encrypted cookie
router.HandleFunc("/login", handleLogin).Methods("POST")
// Serves the admin console page. Only visible to authenticated admins
router.HandleFunc("/admin/manage", handleAdminManage).Methods("GET")
// Serves a page showing details of a specific event. Only visible to admins
router.HandleFunc("/admin/event/{eventID}", handleAdminManageEvent).Methods("GET")
// Handles an API call to get event details
router.HandleFunc("/api/event/{eventID}", handleAPIGetEvent).Methods("GET")

HTTP Middlewares

Creating a header authentication middleware

Middlewares let you apply some functionalities to many HTTP handlers. You can read more about them here.

Since all calls to the API requires an authentication header, it makes sense to write a middleware to do the heavy lifting. We can use this middleware to wrap all the API handlers, so that when a request comes in without the correct Authroization header, we can return a 401 Unauthorized.

⚠️ REALISTICALLY, the header auth middleware should check the header value against a database.

// HeaderAuth creates a new middleware that only serves requests with a matching header
func HeaderAuth(headerKey string, expectedValue string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.Header.Get(headerKey) != expectedValue {
				w.WriteHeader(http.StatusUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

Then we can wrap all our API handlers with this middleware, like this

headerAuth := HeaderAuth("Authorization", "SECRET_API_KEY_HERE")
router.HandleFunc("/api/event/{eventID}", headerAuth(handleAPIGetEvent)).Methods("GET")

Now, if user calls GET /api/event/1234 without the Authorization header, the header auth middleware will return a 401 Unauthorized to the client, without even passing the request to our handleAPIGetEvent.

Similar process for session authentication

Here I’m using gorilla sessions and securecookies.

import "github.com/gorilla/sessions"

// SessionAuth creates a new middleware that only serves requests with a matching
// session key-value in the specified CookieStore.
// It also refreshes the session if user is authorized
func SessionAuth(store *sessions.CookieStore, sessionName string, keyName string, expectedValue string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// get the session from store
			session, err := store.Get(r, sessionName)
			if err != nil {
				WriteError(w, http.StatusUnauthorized)
				return
			}
			// get the cookie from session
			cookieVal := session.Values[keyName]
			cookieValStr, ok := cookieVal.(string)
			if !ok || cookieValStr != expectedValue {
				WriteError(w, http.StatusUnauthorized)
				return
			}
			// refresh the session
			session.Save(r, w)
			next.ServeHTTP(w, r)
		})
	}
}

Then for the admin page handlers, we wrap them in this session auth middleware.

sessionAuth := SessionAuth(store, "login", "user-role", "admin")
router.HandleFunc("/admin/manage", sessionAuth(handleAdminManage)).Methods("GET")

⚠️ Again, REALISTICALLY, you should never store user-role in an encrypted cookie. Instead, store the user ID, and modify the session auth middleware code above to verify the user privilege against a database.

The problem with copy pasting

Now I have to add a new API call, something like /api/guest/{guestID} where it returns all the events that guest has attended. Time to copy paste~

router.HandleFunc("/api/guest/{guestID}", headerAuth(handleAPIGetGuest)).Methods("GET")

How about another one? Like /api/date/{date} to show all the events starting that day. Paste once more 📋

router.HandleFunc("/api/date/{date}", headerAuth(handleAPIGetDate)).Methods("GET")

One of those days you’ll forget to wrap some handler inside headerAuth, and all things will break loose.

Subrouters and prefixes

Gorilla mux lets you create subrouters for your main router to handle different path prefixes. Unfortunately it is not easy to apply different middlewares to different subrouters.

The solution is to create new routers, then route the request from our main router to our new routers (technically subrouters)

For public endpoints, where no middleware is needed, the code remains the same.

router := mux.NewRouter()
router.HandleFunc("/", handleIndex).Methods("GET")
router.HandleFunc("/login", handleLogin).Methods("POST")

Now here is where things get interesting. Let’s make a new router for our admin pages.

// Make a new router for API handlers
apiRouter := mux.NewRouter()
// Create a subrouter so that we don't have to copy paste the /api prefix
apiSub := apiRouter.PathPrefix("/api").Subrouter()
// Add our handlers to API subrouter
apiSub.HandleFunc("/event/{eventID}", handleAPIGetEvent).Methods("GET")
apiSub.HandleFunc("/guest/{guestID}", handleAPIGetGuest).Methods("GET")
apiSub.HandleFunc("/date/{date}", handleAPIGetDate).Methods("GET")
// Add our new router to the main router. Apply the middleware to it ^o^
router.Handle("/api/{_:.*}", headerAuth(apiRouter))

We have a new router apiRouter for our API handlers, which is wrapped in the header auth middleware.

The main router will catch any request whose URL matches the pattern "/api/{_:.*}", and pass them to our apiRouter. Then, apiRouter finds the prefix "/admin", strips that prefix, and routes them to our apiSub, where all our handle routines are.

Same thing with admin page handlers, with session auth middleware

adminRouter := mux.MuxRouter()
adminSub := adminRouter.PathPrefix("/admin").Subrouter()
adminSub.HandleFunc("/manage", handleAdminManage).Methods("GET")
router.Handle("/admin/{_:.*}", sessionAuth(adminSub))

Middlewares can be nested

Because we can chain the middlewares, we can add a LoggingHandler (which logs incoming requests to an io.Writer) to the main router, which will also be applied to all our sub-routers.

So finally, let’s add our router to the default serve mux, and boot up our web server.

handlers.LoggingHandler is provided in "github.com/gorilla/handlers"

func main() {
  // ...
  // code to make the main router and sub-routers
  // ...
  http.Handle("/", handlers.LoggingHandler(os.Stdout, router))
  http.ListenAndServe(":8080", nil)
}

Bonus: another benefit of using middlewares 👍

… is that you can reuse your handlers if needed, but with different middlewares applied.

For example, in one of your admin web pages, you want to use AJAX to get a specifc event.

  • What you want: GET /api/event/42 and receive the event #42 details in JSON format.
  • The problem: /api/event uses header auth, but you can’t just hardcode the auth header in your front-end javascript.
    • But you know your current user has administrator privileges and that they pass the session auth.
  • The solution: Add the same handler handleAPIGetEvent, but this time with headerAuth middleware applied. We can use the admin pages subrouter.
    • We only need to add this one line, before we add the admin pages router to main router
  adminSub.HandleFunc("/ajax-middleware-blah-blah", handleAPIGetEvent)

Same handler, different middleware.