A flexible, Keycloak-based authentication middleware for Go microservices using the Gin web framework. This middleware provides role-based access control (RBAC) and action-based permissions with configuration through YAML files.
- π Keycloak integration for token validation
- π YAML-based configuration
- π¦ Role-based access control (RBAC)
- π― Action-based permissions
- π Redis caching support for token validation
- π£οΈ Path-based permission matching with parameter support
- π§ Environment variable resolution in configuration
- π¨ Flexible permission callback system
go get github.com/null-bd/authn- Create a
config.yamlfile in your service's root directory:
auth:
serviceId: "my-service"
clientId: "my-client"
clientSecret: "${CLIENT_SECRET}"
keycloakUrl: "http://keycloak:8080"
realm: "my-realm"
cacheEnabled: true
cacheUrl: "redis:6379"
resources:
- path: "/api/v1/users"
method: "GET"
roles: ["admin", "user"]
actions: ["read:users"]
publicPaths:
- path: "/health"
methods: ["GET"]
- path: "/api/v1/public/*"
methods: ["GET", "POST"]
- path: "/metrics"
methods: ["GET"]- Initialize the middleware in your service:
package main
import (
"github.com/gin-gonic/gin"
"github.com/null-bd/authn/pkg/authmiddleware"
"github.com/null-bd/logger"
)
func main() {
// Load configuration
configLoader := authmiddleware.NewConfigLoader("config.yaml")
config, err := configLoader.Load()
if err != nil {
log.Fatal(err)
}
// Initialize middleware
authMiddleware, err := authmiddleware.NewAuthMiddleware(logger.Logger, *config, nil)
if err != nil {
log.Fatal(err)
}
// Setup Gin router
r := gin.Default()
r.Use(authMiddleware.Authenticate())
// Define routes
r.Run(":8080")
}The configuration file (config.yaml) supports the following options:
auth:
serviceId: string # Unique identifier for the service
clientId: string # Keycloak client ID
clientSecret: string # Keycloak client secret (supports env vars)
keycloakUrl: string # Keycloak server URL
realm: string # Keycloak realm name
cacheEnabled: bool # Enable Redis caching
cacheUrl: string # Redis server URL
resources: # Array of resource permissions
- path: string # API endpoint path
method: string # HTTP method
roles: string[] # Required roles
actions: string[] # Required actions
serviceId: string # (Optional) Override service IDYou can use environment variables in the configuration file using the ${VAR_NAME} syntax:
auth:
clientSecret: "${KEYCLOAK_CLIENT_SECRET}"
keycloakUrl: "${KEYCLOAK_URL}"The middleware supports path parameters in resource definitions:
resources:
- path: "/api/v1/organizations/{orgId}/users"
method: "GET"
roles: ["admin"]
actions: ["read:users"]Define required roles for each endpoint:
resources:
- path: "/api/v1/users"
method: "POST"
roles: ["admin", "user-manager"]
actions: ["create:users"]Define required actions for fine-grained control:
resources:
- path: "/api/v1/reports"
method: "GET"
roles: ["analyst"]
actions: ["read:reports", "export:data"]You can implement custom permission logic using a callback:
permCallback := func(orgId, branchId, role string) []string {
// Your custom permission logic here
return []string{"read:users", "write:users"}
}
authMiddleware, err := authmiddleware.NewAuthMiddleware(config, permCallback)The middleware adds the following claims to the Gin context:
type TokenClaims struct {
OrgID string `json:"org_id"`
BranchID string `json:"branch_id"`
Roles []string `json:"roles"`
Actions []string `json:"actions"`
}Access claims in your handlers:
func handler(c *gin.Context) {
claims, exists := c.Get("claims")
if !exists {
c.JSON(401, gin.H{"error": "no claims found"})
return
}
tokenClaims := claims.(*TokenClaims)
// Use the claims...
}The middleware provides the following error types:
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token has expired")
ErrInsufficientScope = errors.New("insufficient scope")
ErrMissingToken = errors.New("missing token")
ErrInvalidConfig = errors.New("invalid configuration")
ErrServiceUnavailable = errors.New("auth service unavailable")
)When caching is enabled, token validation results are cached in Redis:
auth:
cacheEnabled: true
cacheUrl: "redis:6379"