API authorization through middlewares

When dealing with API authorization based on access tokens, permissions (user can see a list, can create an item, can delete one etc) and/or account types (administrator, moderator, normal user etc), I’ve seen the approach of checking requirements inside the HTTP handlers functions.

This post and attached source code do not handle security, API design, data storage patterns or any other best practices that do not aim directly at the main subject: Authorization through middlewares. All other code is just for illustrating the idea as a whole.

A classical way of dealing various authorization checks is to verify everything inside the handler function.

type User struct {
   Username    string
   Type        string
   Permissions uint8
}

var CanDoAction uint8 = 1

func tokenIsValid(token string) bool {
   // ...
   return true
}

func getUserHandler(c echo.Context) error {
   // Check authorization token
   token := c.Get("token").(string)
   if !tokenIsValid(token) {
      return c.NoContent(http.StatusUnauthorized)
   }

   user := c.Get("user").(User)

   // Check account type
   if user.Type != "admin" {
      return c.NoContent(http.StatusForbidden)
   }

   // Check permission for handler
   if user.Permissions&CanDoAction == 0 {
      return c.NoContent(http.StatusForbidden)
   }

   // Get data and send it as response
   data := struct {
      Username string `json:"username"`
   }{
      Username: user.Username,
   }

   return c.JSON(http.StatusOK, data)
}

The handler is doing more than its purpose. The getUserHandler should take care of operations around the process of retrieving a user from the storage layer, maybe based on some input data, and send it as response:

  • take data from request if necessary
  • validate data
  • retrieve data from the storage layer
  • send the HTTP response of failure or success

There could be special exceptions, but most of implementations are not that special. And even when you are sure you have an exception, think again, maybe the logic can be moved to a service.

I would like to rewrite the above handler as:

func getUserHandler(c echo.Context) error {
   // Get data and send it as response
   data := struct {
      Username string `json:"username"`
   }{
      Username: user.Username,
   }

   return c.JSON(http.StatusOK, data)
}

All authorization will be moved to middlewares attached to handler registration:

package main

import (
   "github.com/labstack/echo"
   "github.com/labstack/echo/middleware"
)

func main() {
   // ...

   ug := e.Group("/users", AuthorizationMiddleware)
   ug.GET("/:id", getUserHandler, AdminAccessOnlyMiddleware, ACLMiddleware(CanDoAction))

   // ...
}
  • AuthorizationMiddleware will check access token information
  • AdminAccessOnlyMiddleware will grant access only to admins
  • ACLMiddleware will verify specified permissions against the user’s

Echo framework was my case, but it’s the same idea even in pure Go. I’ve written a working implementation for the subject, check it on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.