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.