A lightweight authentication and authorization middleware for Gin-based Dromos Suite APIs, using Zitadel OIDC tokens.
go get github.com/Prescott-Data/dromos-authkitpackage main
import (
"github.com/gin-gonic/gin"
"github.com/Prescott-Data/dromos-authkit"
)
func main() {
r := gin.Default()
// Configure authentication
cfg := authkit.Config{
IssuerURL: "https://your-zitadel-instance.com",
Audience: "your-project-id",
SkipPaths: []string{"/health", "/metrics"},
}
// Apply authentication middleware
r.Use(authkit.AuthN(cfg))
// Protected route
r.GET("/api/user", func(c *gin.Context) {
userID := authkit.UserID(c)
email := authkit.Email(c)
c.JSON(200, gin.H{
"user_id": userID,
"email": email,
})
})
r.Run(":8080")
}The AuthN middleware validates JWT tokens and extracts claims:
r.Use(authkit.AuthN(authkit.Config{
IssuerURL: "https://zitadel.example.com",
Audience: "123456789@project_name",
}))Require specific roles for protected routes:
// Require admin role
adminRoutes := r.Group("/admin")
adminRoutes.Use(authkit.RequireRole("admin"))
{
adminRoutes.GET("/users", listUsers)
adminRoutes.DELETE("/users/:id", deleteUser)
}
// Require any of multiple roles
editorRoutes := r.Group("/content")
editorRoutes.Use(authkit.RequireRole("editor", "admin"))
{
editorRoutes.POST("/articles", createArticle)
}r.GET("/profile", func(c *gin.Context) {
// Get individual fields
userID := authkit.UserID(c)
email := authkit.Email(c)
orgID := authkit.OrgID(c)
// Get full claims
claims := authkit.GetClaims(c)
c.JSON(200, gin.H{
"user_id": userID,
"email": email,
"org_id": orgID,
"roles": claims.Roles,
})
})r.POST("/publish", func(c *gin.Context) {
if !authkit.HasRole(c, "publisher") {
c.JSON(403, gin.H{"error": "insufficient permissions"})
return
}
// Process publication
c.JSON(200, gin.H{"status": "published"})
})
// Check multiple roles
if authkit.HasAnyRole(c, "admin", "moderator") {
// Allow access
}r.GET("/api/data", func(c *gin.Context) {
orgID := authkit.OrgID(c)
// Filter data by organization
data := fetchDataForOrg(orgID)
c.JSON(200, data)
})// Ensure user belongs to specific tenant
r.Use(authkit.RequireTenant("expected-org-id"))
// Or allow any tenant but store for later use
r.Use(authkit.AuthN(cfg))
r.GET("/data", func(c *gin.Context) {
// OrgID is automatically extracted
tenantID := authkit.OrgID(c)
// ...
})cfg := authkit.Config{
IssuerURL: "https://zitadel.example.com",
Audience: "project-id",
SkipPaths: []string{
"/health",
"/metrics",
"/api/v1/public/*",
},
}
r.Use(authkit.AuthN(cfg))For WebSocket connections, tokens can be passed via query parameter:
// Client: ws://localhost:8080/ws?token=eyJhbGc...
r.GET("/ws", func(c *gin.Context) {
// Token automatically extracted from query param
userID := authkit.UserID(c)
// Upgrade to WebSocket
upgrader.Upgrade(c.Writer, c.Request, nil)
})type Config struct {
IssuerURL string // Zitadel issuer URL
Audience string // Expected audience (project ID)
SkipPaths []string // Routes that bypass auth
}AuthN(cfg Config) gin.HandlerFunc- Authentication middlewareRequireRole(roles ...string) gin.HandlerFunc- Authorization middlewareRequireTenant(tenantID string) gin.HandlerFunc- Tenant validation middleware
GetClaims(c *gin.Context) *Claims- Retrieve full claims objectUserID(c *gin.Context) string- Get authenticated user IDEmail(c *gin.Context) string- Get user emailOrgID(c *gin.Context) string- Get organization IDHasRole(c *gin.Context, role string) bool- Check single roleHasAnyRole(c *gin.Context, roles ...string) bool- Check multiple roles
type Claims struct {
Sub string // User ID
Email string // User email
OrgID string // Organization ID
Roles map[string]interface{} // Role assignments
}import "os"
cfg := authkit.Config{
IssuerURL: os.Getenv("ZITADEL_ISSUER_URL"),
Audience: os.Getenv("ZITADEL_AUDIENCE"),
SkipPaths: []string{"/health"},
}The middleware returns standard HTTP error codes:
- 401 Unauthorized - Missing or invalid token
- 403 Forbidden - Valid token but insufficient permissions
r.Use(func(c *gin.Context) {
c.Next()
if c.Writer.Status() == 401 {
// Log authentication failure
}
})import (
"testing"
"net/http/httptest"
"github.com/gin-gonic/gin"
)
func TestProtectedRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
// Mock or skip auth for tests
cfg := authkit.Config{
IssuerURL: "https://test.zitadel.com",
Audience: "test-project",
SkipPaths: []string{"/test"},
}
r.Use(authkit.AuthN(cfg))
r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/test", nil)
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}