Gin Authorization
In web applications, authorization is the process of determining whether an authenticated user has permission to access specific resources or perform certain actions. While authentication confirms a user's identity, authorization deals with what they're allowed to do within the application.
In this guide, we'll explore how to implement authorization in Gin web applications, building upon the authentication concepts we've already covered.
Introduction to Authorization in Gin
Authorization in Gin typically follows these principles:
- A user authenticates (proves their identity)
- The application determines what resources/actions the user can access
- Middleware or handlers check permissions before allowing access
Gin doesn't provide built-in authorization functionality, but it offers a flexible middleware system that makes implementing custom authorization solutions straightforward.
Basic Role-Based Authorization
One of the simplest authorization approaches is role-based access control (RBAC), where permissions are assigned to roles, and users are assigned to those roles.
Creating a Role-Based Middleware
Let's create a middleware that restricts routes to users with specific roles:
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// RequireRole returns a middleware that ensures the user has the specified role
func RequireRole(role string) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user from the context (set during authentication)
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized: User not found in context",
})
c.Abort()
return
}
// Type assertion to get our user struct
user, ok := userVal.(User)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal error: User type assertion failed",
})
c.Abort()
return
}
// Check if user has the required role
hasRole := false
for _, r := range user.Roles {
if r == role {
hasRole = true
break
}
}
if !hasRole {
c.JSON(http.StatusForbidden, gin.H{
"error": "Forbidden: You don't have the required role",
})
c.Abort()
return
}
c.Next()
}
}
Using the Role-Based Middleware
Here's how to use this middleware to protect routes:
func main() {
router := gin.Default()
// Public routes
router.GET("/", publicHandler)
// Auth routes
auth := router.Group("/")
auth.Use(middleware.RequireAuthentication())
{
// Routes for any authenticated user
auth.GET("/profile", getProfile)
// Admin only routes
admin := auth.Group("/admin")
admin.Use(middleware.RequireRole("admin"))
{
admin.GET("/users", listAllUsers)
admin.DELETE("/users/:id", deleteUser)
}
// Editor only routes
editor := auth.Group("/content")
editor.Use(middleware.RequireRole("editor"))
{
editor.POST("/articles", createArticle)
editor.PUT("/articles/:id", updateArticle)
}
}
router.Run(":8080")
}
Permission-Based Authorization
For more granular control, you might need permission-based authorization, which focuses on specific actions rather than broad roles.
Implementing Permission Checking
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Permission represents a specific action a user can perform
type Permission string
const (
PermCreateUser Permission = "create:user"
PermReadUser Permission = "read:user"
PermUpdateUser Permission = "update:user"
PermDeleteUser Permission = "delete:user"
PermCreateArticle Permission = "create:article"
PermReadArticle Permission = "read:article"
PermUpdateArticle Permission = "update:article"
PermDeleteArticle Permission = "delete:article"
)
// RequirePermission returns a middleware that ensures the user has the specified permission
func RequirePermission(perm Permission) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user from context
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized: User not found in context",
})
c.Abort()
return
}
user, ok := userVal.(User)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal error: User type assertion failed",
})
c.Abort()
return
}
// Check if user has the permission
hasPermission := false
for _, p := range user.Permissions {
if Permission(p) == perm {
hasPermission = true
break
}
}
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{
"error": "Forbidden: You don't have the required permission",
})
c.Abort()
return
}
c.Next()
}
}
Using Permission-Based Authorization
func main() {
router := gin.Default()
auth := router.Group("/")
auth.Use(middleware.RequireAuthentication())
{
// User management endpoints
users := auth.Group("/users")
{
users.GET("/", middleware.RequirePermission(middleware.PermReadUser), listUsers)
users.POST("/", middleware.RequirePermission(middleware.PermCreateUser), createUser)
users.PUT("/:id", middleware.RequirePermission(middleware.PermUpdateUser), updateUser)
users.DELETE("/:id", middleware.RequirePermission(middleware.PermDeleteUser), deleteUser)
}
// Article endpoints
articles := auth.Group("/articles")
{
articles.GET("/", middleware.RequirePermission(middleware.PermReadArticle), listArticles)
articles.POST("/", middleware.RequirePermission(middleware.PermCreateArticle), createArticle)
articles.PUT("/:id", middleware.RequirePermission(middleware.PermUpdateArticle), updateArticle)
articles.DELETE("/:id", middleware.RequirePermission(middleware.PermDeleteArticle), deleteArticle)
}
}
router.Run(":8080")
}
Resource-Based Authorization
Resource-based authorization ensures that users can only access specific resources they own or are assigned to.
Example: Ensuring Users Can Only Access Their Own Data
// Owner middleware ensures a user can only access their own resources
func EnsureOwner() gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from context
userVal, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
user, ok := userVal.(User)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal error"})
c.Abort()
return
}
// Get resource ID from URL parameters
resourceID := c.Param("id")
// Query the database to check ownership
resource, err := db.GetResourceByID(resourceID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Resource not found"})
c.Abort()
return
}
// Check if the user is the owner
if resource.OwnerID != user.ID {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied: You don't own this resource"})
c.Abort()
return
}
c.Next()
}
}
Using Resource-Based Authorization
func main() {
router := gin.Default()
auth := router.Group("/")
auth.Use(middleware.RequireAuthentication())
{
// Public documents
auth.GET("/documents", listAllDocuments)
// Personal documents - ensure ownership
auth.GET("/documents/:id", middleware.EnsureOwner(), getDocument)
auth.PUT("/documents/:id", middleware.EnsureOwner(), updateDocument)
auth.DELETE("/documents/:id", middleware.EnsureOwner(), deleteDocument)
}
router.Run(":8080")
}
Combining Authorization Approaches
For complex applications, you might need to combine multiple authorization approaches.
Example: Multi-Level Authorization
func main() {
router := gin.Default()
// Authentication middleware
auth := router.Group("/")
auth.Use(middleware.RequireAuthentication())
{
// Documents routes with various authorization levels
documents := auth.Group("/documents")
{
// Anyone can list and create documents
documents.GET("/", listDocuments)
documents.POST("/", createDocument)
// User must own the document OR be an admin
documents.GET("/:id", middleware.OrAuthorization(
middleware.EnsureOwner(),
middleware.RequireRole("admin"),
), getDocument)
// User must own the document OR have edit permission
documents.PUT("/:id", middleware.OrAuthorization(
middleware.EnsureOwner(),
middleware.RequirePermission(middleware.PermEditAnyDocument),
), updateDocument)
// User must own the document OR be an admin
documents.DELETE("/:id", middleware.OrAuthorization(
middleware.EnsureOwner(),
middleware.RequireRole("admin"),
), deleteDocument)
}
}
router.Run(":8080")
}
// OrAuthorization executes middlewares until one succeeds
func OrAuthorization(middlewares ...gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
for _, m := range middlewares {
// Create a copy of the context
cCopy := c.Copy()
// Flag to check if middleware aborted
aborted := false
// Override the Abort function
cCopyAbort := cCopy.Abort
cCopy.Abort = func() {
aborted = true
cCopyAbort()
}
// Execute middleware
m(cCopy)
// If middleware didn't abort, we have authorization
if !aborted {
c.Next()
return
}
}
// If we get here, all middlewares failed
c.JSON(http.StatusForbidden, gin.H{
"error": "You don't have permission to access this resource",
})
c.Abort()
}
}
Real-World Example: Blog Platform
Let's create a more complete example of a blog platform with different user roles:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/your-username/blog-app/middleware"
"github.com/your-username/blog-app/models"
)
func main() {
router := gin.Default()
// Public routes
public := router.Group("/")
{
public.GET("/", homePage)
public.GET("/articles", listPublishedArticles)
public.GET("/articles/:id", getPublishedArticle)
public.POST("/login", login)
public.POST("/register", register)
}
// Protected routes - require authentication
protected := router.Group("/")
protected.Use(middleware.RequireAuthentication())
{
// Routes for all authenticated users
protected.GET("/profile", getProfile)
protected.PUT("/profile", updateProfile)
// Writer routes
writer := protected.Group("/writer")
writer.Use(middleware.RequireRole("writer"))
{
writer.GET("/articles", listMyArticles)
writer.POST("/articles", createArticle)
writer.PUT("/articles/:id", middleware.EnsureArticleOwner(), updateArticle)
writer.DELETE("/articles/:id", middleware.EnsureArticleOwner(), deleteArticle)
}
// Editor routes
editor := protected.Group("/editor")
editor.Use(middleware.RequireRole("editor"))
{
editor.GET("/pending", listPendingArticles)
editor.PUT("/articles/:id/review", reviewArticle)
}
// Admin routes
admin := protected.Group("/admin")
admin.Use(middleware.RequireRole("admin"))
{
admin.GET("/users", listAllUsers)
admin.PUT("/users/:id/roles", updateUserRoles)
admin.DELETE("/users/:id", deleteUser)
admin.GET("/articles", listAllArticles)
admin.DELETE("/articles/:id", adminDeleteArticle)
}
}
router.Run(":8080")
}
// Definition for middleware.EnsureArticleOwner()
func EnsureArticleOwner() gin.HandlerFunc {
return func(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(models.User)
articleID := c.Param("id")
article, err := models.GetArticleByID(articleID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
c.Abort()
return
}
if article.AuthorID != user.ID {
c.JSON(http.StatusForbidden, gin.H{
"error": "You don't have permission to modify this article",
})
c.Abort()
return
}
c.Next()
}
}
In this example:
- Public users can view published articles
- Writers can create and manage their own articles
- Editors can review pending articles
- Admins have full access to manage users and all articles
Best Practices for Authorization in Gin
- Separate concerns: Keep authentication and authorization logic separate
- Fail securely: Always default to denying access when in doubt
- Use middleware chains: Combine middlewares for complex authorization scenarios
- Check authorization early: Validate permissions before processing requests
- Granular permissions: Use fine-grained permissions rather than broad roles when needed
- Log authorization failures: Keep audit logs of access attempts and failures
- Keep authorization logic DRY: Create reusable middleware functions
- Test thoroughly: Write test cases for all authorization scenarios
Common Authorization Patterns
1. JWT Claims-Based Authorization
With JWT-based authentication, you can include roles or permissions directly in token claims:
// Middleware to check JWT claims for permissions
func RequirePermissionFromJWT(permission string) gin.HandlerFunc {
return func(c *gin.Context) {
// Get claims from JWT token
claims, exists := c.Get("claims")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
c.Abort()
return
}
// Extract permissions from claims
tokenClaims, ok := claims.(jwt.MapClaims)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid claims format"})
c.Abort()
return
}
// Get permissions from claims
perms, ok := tokenClaims["permissions"].([]interface{})
if !ok {
c.JSON(http.StatusForbidden, gin.H{"error": "No permissions found"})
c.Abort()
return
}
// Check if permission exists
hasPermission := false
for _, p := range perms {
if p.(string) == permission {
hasPermission = true
break
}
}
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
c.Abort()
return
}
c.Next()
}
}
2. Database-Driven Authorization
For dynamic permissions that might change during a user's session, you might need to check the database:
func RequireDynamicPermission(permission string) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
c.Abort()
return
}
// Check permission in database
hasPermission, err := models.CheckUserPermission(userID.(uint), permission)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error checking permissions"})
c.Abort()
return
}
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
c.Abort()
return
}
c.Next()
}
}
Summary
In this guide, we've explored how to implement authorization in Gin web applications:
- Role-based authorization: Restricting access based on user roles
- Permission-based authorization: Finer-grained control over specific actions
- Resource-based authorization: Ensuring users can only access their own resources
- Combined approaches: Implementing complex authorization logic
- Real-world example: A blog platform with different user roles and permissions
Authorization is a critical component of web application security, working alongside authentication to ensure that users can only access the resources and actions they're permitted to use.
Additional Resources
- Gin Framework documentation
- OWASP Authorization Cheat Sheet
- Role-Based Access Control (RBAC) explained
- Attribute-Based Access Control (ABAC)
Exercises
-
Implement a simple blog API with two roles: "user" and "admin". Users should only be able to edit their own posts, while admins can edit any post.
-
Create a middleware that combines role and permission checks, where certain actions require both a specific role and a specific permission.
-
Implement resource-based authorization for a todo list application where users can only see and modify their own todo items.
-
Create a "super admin" bypass that allows super admins to access any route regardless of other authorization checks.
-
Implement an audit logging system that records all authorization failures with the user ID, requested resource, and timestamp.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)