Skip to content

Prescott-Data/dromos-authkit

Repository files navigation

Dromos AuthKit

License: Proprietary

A lightweight authentication and authorization middleware for Gin-based Dromos Suite APIs, using Zitadel OIDC tokens.

Installation

go get github.com/Prescott-Data/dromos-authkit

Quick Start

package 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")
}

Usage Examples

Basic Authentication

The AuthN middleware validates JWT tokens and extracts claims:

r.Use(authkit.AuthN(authkit.Config{
    IssuerURL: "https://zitadel.example.com",
    Audience:  "123456789@project_name",
}))

Role-Based Authorization

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)
}

Accessing User Information

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,
    })
})

Custom Role Checks

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
}

Multi-Tenant Applications

r.GET("/api/data", func(c *gin.Context) {
    orgID := authkit.OrgID(c)

    // Filter data by organization
    data := fetchDataForOrg(orgID)

    c.JSON(200, data)
})

Tenant-Scoped Middleware

// 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)
    // ...
})

Skip Authentication for Specific Routes

cfg := authkit.Config{
    IssuerURL: "https://zitadel.example.com",
    Audience:  "project-id",
    SkipPaths: []string{
        "/health",
        "/metrics",
        "/api/v1/public/*",
    },
}

r.Use(authkit.AuthN(cfg))

WebSocket Authentication

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)
})

API Reference

Configuration

type Config struct {
    IssuerURL string   // Zitadel issuer URL
    Audience  string   // Expected audience (project ID)
    SkipPaths []string // Routes that bypass auth
}

Middleware

  • AuthN(cfg Config) gin.HandlerFunc - Authentication middleware
  • RequireRole(roles ...string) gin.HandlerFunc - Authorization middleware
  • RequireTenant(tenantID string) gin.HandlerFunc - Tenant validation middleware

Claims Functions

  • GetClaims(c *gin.Context) *Claims - Retrieve full claims object
  • UserID(c *gin.Context) string - Get authenticated user ID
  • Email(c *gin.Context) string - Get user email
  • OrgID(c *gin.Context) string - Get organization ID
  • HasRole(c *gin.Context, role string) bool - Check single role
  • HasAnyRole(c *gin.Context, roles ...string) bool - Check multiple roles

Claims Structure

type Claims struct {
    Sub   string                 // User ID
    Email string                 // User email
    OrgID string                 // Organization ID
    Roles map[string]interface{} // Role assignments
}

Configuration with Environment Variables

import "os"

cfg := authkit.Config{
    IssuerURL: os.Getenv("ZITADEL_ISSUER_URL"),
    Audience:  os.Getenv("ZITADEL_AUDIENCE"),
    SkipPaths: []string{"/health"},
}

Error Handling

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
    }
})

Testing

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)
    }
}

About

shared AuthN / AuthZ Go module

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Contributors