I'm writing a Go web API which uses JWT for tokens/auth etc. and I was wondering if there was a more elegant way of providing varying levels of access to my route handlers than the following?
package main
import (
"net/http"
"github.com/gin-gonic/gin"
jwt "gopkg.in/appleboy/gin-jwt.v2"
)
var (
users = map[string]byte{
"admin": 1,
"manager": 2,
"employee": 3,
}
)
func main() {
router := gin.Default()
group := router.Group("/v1")
jwtMiddleware := &jwt.GinJWTMiddleware{
Realm: "realm",
Key: []byte("password"),
Authenticator: authenticate,
PayloadFunc: payload,
}
group.Use(jwtMiddleware.MiddlewareFunc())
group.GET("/refreshToken", jwtMiddleware.RefreshHandler)
group.GET("/hello", hello)
router.POST("/login", jwtMiddleware.LoginHandler)
router.Run(":1234")
}
func hello(c *gin.Context) {
switch getRoleFromContext(c) {
case 1:
helloAdmin(c)
case 2:
helloManager(c)
case 3:
helloEmployee(c)
default:
c.AbortWithStatus(http.StatusForbidden)
}
}
func helloAdmin(c *gin.Context) { c.String(http.StatusOK, "Hello, admin!") }
func helloManager(c *gin.Context) { c.String(http.StatusOK, "Hello, manager!") }
func helloEmployee(c *gin.Context) { c.String(http.StatusOK, "Hello, employee!") }
func authenticate(email string, password string, c *gin.Context) (string, bool) {
if _, ok := users[email]; ok {
return email, true
}
return "", false
}
func payload(email string) map[string]interface{} {
return map[string]interface{}{
"role": users[email],
}
}
func getRoleFromContext(c *gin.Context) (role byte) {
claims := jwt.ExtractClaims(c)
rawRole, ok := claims["role"]
if !ok {
c.AbortWithStatus(http.StatusForbidden)
}
floatRole, ok := rawRole.(float64)
if !ok {
c.AbortWithStatus(http.StatusForbidden)
}
return byte(floatRole)
}
In my real code (consisting of only 18 or so routes), I've created a Routes struct, which provides handlers for all of the routes my API provides. Within that, I'm performing the above switching logic to specialised Route structs, which ensures only administrative users can perform specific functions, while other functions are callable by multiple types of users, just with different behaviour.
I've tried throwing together middleware-based auth as follows:
func roleCheckerMiddleware(roles ...byte) gin.HandlerFunc {
return func(c *gin.Context) {
contextRole := getRoleFromContext(c)
for _, role := range roles {
if contextRole == role {
c.Next()
return
}
}
c.AbortWithStatus(http.StatusForbidden)
}
}
...and applying it as follows:
adminGroup := group.Group("/")
adminGroup.GET("/hello", helloAdmin)
adminGroup.Use(roleCheckerMiddleware(1))
managerGroup := group.Group("/")
managerGroup.GET("/hello", helloManager)
managerGroup.Use(roleCheckerMiddleware(2, 1))
employeeGroup := group.Group("/")
employeeGroup.GET("/hello", helloEmployee)
employeeGroup.Use(roleCheckerMiddleware(3, 2, 1))
...but the obvious duplication of the /hello route means this method isn't (as far as I'm aware) possible, without the introduction of groupings for /admin, /manager and /employee. I'd love to be shown otherwise though as this feels more elegant that my switching logic!