Skip to content

companyinfo/keycloak

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Keycloak Go Client

CI codecov Go Reference Go Report Card Go Version License

A production-ready, idiomatic Go client for the Keycloak Admin API.

Quick Links: Installation | Quick Start | Examples | API Docs

Overview

The package provides a clean, type-safe interface for interacting with Keycloak's Admin API. Built for production use, it handles authentication, automatic token refresh, retry logic, and provides comprehensive resource management capabilities.

Why This Library?

  • Production Ready: Battle-tested with automatic token management, retry logic, and comprehensive error handling
  • Type Safe: Strongly typed models prevent runtime errors and improve code maintainability
  • Modern Go Patterns: Context support, functional options, and resource-based client design
  • Well Tested: Extensive test coverage with unit, mock, and integration tests
  • Easy to Use: Simple API with sensible defaults, yet flexible for advanced use cases

Table of Contents

Features

  • Authentication: OAuth2 client credentials flow with automatic token management and refresh
  • Group Management: Complete CRUD operations for groups and subgroups with attribute support
  • Pagination: Built-in support for paginated requests with configurable page sizes
  • Type Safety: Strongly typed models and interfaces prevent runtime errors
  • Context Support: All operations accept context for cancellation, timeout control, and request tracing
  • Retry Logic: Configurable exponential backoff for handling transient failures
  • Flexible Configuration: Functional options pattern for easy customization
  • Production Ready: Debug logging, custom headers, proxy support, and comprehensive error handling

Requirements

  • Go: 1.24 or later
  • Keycloak: 26.x or later (may work with earlier versions but not officially tested)
  • Client Credentials: A Keycloak client with appropriate permissions:
    • Service accounts enabled
    • Client authentication: ON (confidential client)
    • At minimum: view-users, manage-users, manage-groups roles
    • For full admin operations: realm-admin role

💡 Tip: Check Keycloak Client Setup for detailed configuration steps.

Installation

go get go.companyinfo.dev/keycloak

The package name is keycloak, so you'll use it as:

import "go.companyinfo.dev/keycloak"

client, err := keycloak.New(ctx, keycloak.Config{...})

📝 Note: Some examples in the wild may use keycloak as an import alias, but it's not necessary. The package name is keycloak.

Quick Start

Get up and running in 60 seconds:

package main

import (
    "context"
    "fmt"
    "log"
    "time"
    
    "go.companyinfo.dev/keycloak"
)

func main() {
    ctx := context.Background()
    
    // 1. Create client with production-ready defaults
    client, err := keycloak.New(ctx, keycloak.Config{
        URL:          "https://keycloak.example.com",
        Realm:        "my-realm",
        ClientID:     "admin-cli",
        ClientSecret: "your-client-secret",
    },
        keycloak.WithTimeout(30*time.Second),                    // Prevent hanging
        keycloak.WithRetry(3, 1*time.Second, 10*time.Second),   // Handle transient failures
    )
    if err != nil {
        log.Fatalf("Failed to initialize client: %v", err)
    }
    
    // 2. Create a group
    groupID, err := client.Groups.Create(ctx, "Engineering", map[string][]string{
        "department": {"engineering"},
        "location":   {"remote"},
    })
    if err != nil {
        log.Fatalf("Failed to create group: %v", err)
    }
    
    fmt.Printf("✓ Created group: %s\n", groupID)
    
    // 3. List groups
    groups, err := client.Groups.List(ctx, nil, false)
    if err != nil {
        log.Fatalf("Failed to list groups: %v", err)
    }
    
    fmt.Printf("✓ Found %d groups\n", len(groups))
}

That's it! Continue reading for advanced features and best practices.

Usage

Basic Setup

package main

import (
    "context"
    "log"
    
    "go.companyinfo.dev/keycloak"
)

func main() {
    ctx := context.Background()
    
    config := keycloak.Config{
        URL:          "https://keycloak.example.com",
        Realm:        "my-realm",
        ClientID:     "admin-cli",
        ClientSecret: "your-client-secret",
    }
    
    client, err := keycloak.New(ctx, config)
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }
    
    // Access resource-specific clients
    // client.Groups - for group operations
    // Future: client.Users, client.Roles, client.Organizations, etc.
}

Advanced Configuration with Options

The client supports functional options for flexible configuration:

import (
    "context"
    "log"
    "time"
    
    "go.companyinfo.dev/keycloak"
)

func main() {
    ctx := context.Background()
    
    config := keycloak.Config{
        URL:          "https://keycloak.example.com",
        Realm:        "my-realm",
        ClientID:     "admin-cli",
        ClientSecret: "your-client-secret",
    }
    
    client, err := keycloak.New(ctx, config,
        keycloak.WithPageSize(100),                                  // Custom page size
        keycloak.WithTimeout(30*time.Second),                        // Request timeout
        keycloak.WithRetry(3, 5*time.Second, 30*time.Second),       // Retry configuration
        keycloak.WithDebug(true),                                    // Enable debug logging
        keycloak.WithUserAgent("my-app/1.0"),                        // Custom User-Agent
        keycloak.WithHeaders(map[string]string{                      // Custom headers
            "X-Request-ID": "12345",
        }),
    )
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }
    
    // Client is ready with custom configuration
}

Available Options

  • WithPageSize(size int) - Set default page size for paginated requests (default: 50)
  • WithTimeout(timeout time.Duration) - Set request timeout for all API calls
  • WithRetry(count int, waitTime, maxWaitTime time.Duration) - Configure retry behavior
  • WithDebug(debug bool) - Enable debug logging for requests and responses
  • WithHeaders(headers map[string]string) - Add custom headers to all requests
  • WithUserAgent(userAgent string) - Set custom User-Agent header
  • WithProxy(proxyURL string) - Set proxy URL for all requests
  • WithHTTPClient(httpClient *http.Client) - Use custom HTTP client (advanced)

Creating a Group

attributes := map[string][]string{
    "description": {"My group description"},
    "type":        {"organization"},
}

groupID, err := client.Groups.Create(ctx, "My Group", attributes)
if err != nil {
    log.Fatalf("Failed to create group: %v", err)
}

log.Printf("Created group with ID: %s", groupID)

Getting Groups

// Get all groups
groups, err := client.Groups.List(ctx, nil, false)
if err != nil {
    log.Fatalf("Failed to get groups: %v", err)
}

// Get groups with search
searchTerm := "My Group"
groups, err := client.Groups.List(ctx, &searchTerm, false)
if err != nil {
    log.Fatalf("Failed to search groups: %v", err)
}

// Get groups with pagination
groups, err := client.Groups.ListPaginated(ctx, nil, false, 0, 10)
if err != nil {
    log.Fatalf("Failed to get paginated groups: %v", err)
}

// Get group by ID
group, err := client.Groups.Get(ctx, groupID)
if err != nil {
    log.Fatalf("Failed to get group: %v", err)
}

// Count groups
count, err := client.Groups.Count(ctx, nil, nil)
if err != nil {
    log.Fatalf("Failed to count groups: %v", err)
}
log.Printf("Total groups: %d", count)

Working with Group Attributes

// Get group by specific attribute
attribute := &keycloak.GroupAttribute{
    Key:   "salesforceID",
    Value: "SF-12345",
}

group, err := client.Groups.GetByAttribute(ctx, attribute)
if err != nil {
    log.Fatalf("Failed to find group: %v", err)
}

Managing Subgroups

// Create a subgroup
subGroupID, err := client.Groups.CreateSubGroup(ctx, parentGroupID, "Sub Group", attributes)
if err != nil {
    log.Fatalf("Failed to create subgroup: %v", err)
}

// Get subgroups
subGroups, err := client.Groups.ListSubGroups(ctx, parentGroupID)
if err != nil {
    log.Fatalf("Failed to get subgroups: %v", err)
}

// Get subgroup by ID
subGroup, err := client.Groups.GetSubGroupByID(parentGroup, subGroupID)
if err != nil {
    log.Fatalf("Failed to find subgroup: %v", err)
}

Updating and Deleting Groups

// Update a group
group, err := client.Groups.Get(ctx, groupID)
if err != nil {
    log.Fatalf("Failed to get group: %v", err)
}

// Modify group attributes
(*group.Attributes)["updated"] = []string{"true"}

err = client.Groups.Update(ctx, *group)
if err != nil {
    log.Fatalf("Failed to update group: %v", err)
}

// Delete a group
err = client.Groups.Delete(ctx, groupID)
if err != nil {
    log.Fatalf("Failed to delete group: %v", err)
}

Real-World Examples

Example 1: Sync External System with Keycloak Groups

View complete example - Sync departments from HR system to Keycloak
// Sync departments from your HR system to Keycloak
func syncDepartments(ctx context.Context, client *keycloak.Client, departments []Department) error {
    for _, dept := range departments {
        // Try to find existing group by external ID
        attr := &keycloak.GroupAttribute{
            Key:   "externalID",
            Value: dept.ExternalID,
        }
        
        group, err := client.Groups.GetByAttribute(ctx, attr)
        if err == keycloak.ErrGroupNotFound {
            // Create new group
            attributes := map[string][]string{
                "externalID":  {dept.ExternalID},
                "syncedAt":    {time.Now().Format(time.RFC3339)},
                "description": {dept.Description},
            }
            
            _, err := client.Groups.Create(ctx, dept.Name, attributes)
            if err != nil {
                return fmt.Errorf("create group %s: %w", dept.Name, err)
            }
            log.Printf("Created group: %s", dept.Name)
        } else if err != nil {
            return fmt.Errorf("lookup group %s: %w", dept.Name, err)
        } else {
            // Update existing group
            (*group.Attributes)["syncedAt"] = []string{time.Now().Format(time.RFC3339)}
            (*group.Attributes)["description"] = []string{dept.Description}
            
            if err := client.Groups.Update(ctx, *group); err != nil {
                return fmt.Errorf("update group %s: %w", dept.Name, err)
            }
            log.Printf("Updated group: %s", dept.Name)
        }
    }
    return nil
}

Example 2: Bulk Operations with Proper Error Handling

View complete example - Create multiple groups with rollback on failure
// Create multiple groups with rollback on failure
func createOrganizationStructure(ctx context.Context, client *keycloak.Client) error {
    var createdGroups []string
    defer func() {
        if err := recover(); err != nil {
            // Cleanup on panic
            for _, groupID := range createdGroups {
                _ = client.Groups.Delete(ctx, groupID)
            }
        }
    }()
    
    // Create parent organization
    orgID, err := client.Groups.Create(ctx, "Acme Corp", map[string][]string{
        "type": {"organization"},
    })
    if err != nil {
        return fmt.Errorf("create organization: %w", err)
    }
    createdGroups = append(createdGroups, orgID)
    
    // Create departments
    departments := []string{"Engineering", "Sales", "Marketing"}
    for _, dept := range departments {
        deptID, err := client.Groups.CreateSubGroup(ctx, orgID, dept, map[string][]string{
            "type": {"department"},
        })
        if err != nil {
            // Rollback all created groups
            for _, id := range createdGroups {
                _ = client.Groups.Delete(ctx, id)
            }
            return fmt.Errorf("create department %s: %w", dept, err)
        }
        createdGroups = append(createdGroups, deptID)
    }
    
    log.Printf("Successfully created organization with %d departments", len(departments))
    return nil
}

Example 3: Context with Timeout and Cancellation

View complete example - Graceful shutdown with context cancellation
// Graceful shutdown with context cancellation
func processGroupsWithCancellation(ctx context.Context, client *keycloak.Client) error {
    // Create a context with timeout
    ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
    defer cancel()
    
    // Listen for interrupt signal
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    
    go func() {
        <-sigChan
        log.Println("Interrupt received, cancelling operations...")
        cancel()
    }()
    
    // Process groups page by page
    first := 0
    max := 100
    
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            groups, err := client.Groups.ListPaginated(ctx, nil, false, first, max)
            if err != nil {
                return fmt.Errorf("list groups (page %d): %w", first/max, err)
            }
            
            if len(groups) == 0 {
                break // No more groups
            }
            
            // Process this batch
            for _, group := range groups {
                // Process each group - your custom logic here
                log.Printf("Processing group: %s", *group.Name)
                
                // Example: Update group attributes
                if group.Attributes == nil {
                    attrs := make(map[string][]string)
                    group.Attributes = &attrs
                }
                (*group.Attributes)["processed"] = []string{time.Now().Format(time.RFC3339)}
                
                if err := client.Groups.Update(ctx, *group); err != nil {
                    return fmt.Errorf("failed to update group %s: %w", *group.ID, err)
                }
            }
            
            first += max
        }
    }
    
    return nil
}

Example 4: Production Service with Dependency Injection

View complete example - Service pattern with dependency injection and structured logging
// Service struct for dependency injection
type KeycloakService struct {
    client *keycloak.Client
    logger *slog.Logger
}

func NewKeycloakService(ctx context.Context, cfg Config, logger *slog.Logger) (*KeycloakService, error) {
    client, err := keycloak.New(ctx, keycloak.Config{
        URL:          cfg.KeycloakURL,
        Realm:        cfg.Realm,
        ClientID:     cfg.ClientID,
        ClientSecret: cfg.ClientSecret,
    },
        keycloak.WithTimeout(30*time.Second),
        keycloak.WithRetry(3, 1*time.Second, 10*time.Second),
        keycloak.WithUserAgent(fmt.Sprintf("myapp/%s", cfg.Version)),
    )
    if err != nil {
        return nil, fmt.Errorf("initialize keycloak client: %w", err)
    }
    
    return &KeycloakService{
        client: client,
        logger: logger,
    }, nil
}

func (s *KeycloakService) GetOrCreateGroup(ctx context.Context, name string) (*keycloak.Group, error) {
    s.logger.InfoContext(ctx, "looking up group", "name", name)
    
    // Try to find by name
    groups, err := s.client.Groups.List(ctx, &name, false)
    if err != nil {
        s.logger.ErrorContext(ctx, "failed to search groups", "error", err)
        return nil, fmt.Errorf("search groups: %w", err)
    }
    
    for _, group := range groups {
        if *group.Name == name {
            s.logger.InfoContext(ctx, "found existing group", "id", *group.ID)
            return group, nil
        }
    }
    
    // Create if not found
    groupID, err := s.client.Groups.Create(ctx, name, nil)
    if err != nil {
        s.logger.ErrorContext(ctx, "failed to create group", "name", name, "error", err)
        return nil, fmt.Errorf("create group: %w", err)
    }
    
    s.logger.InfoContext(ctx, "created new group", "id", groupID)
    return s.client.Groups.Get(ctx, groupID)
}

API Reference

Client Structure

The Client struct provides access to resource-specific clients:

type Client struct {
    Groups GroupsClient  // Group management operations
    // Future: Users, Roles, Organizations, etc.
}

GroupsClient Interface

The GroupsClient provides methods for managing Keycloak groups:

Group Operations

  • Create(ctx, name, attributes) (string, error) - Create a new group
  • Update(ctx, group) error - Update an existing group
  • Delete(ctx, groupID) error - Delete a group
  • Get(ctx, groupID) (*Group, error) - Get group by ID
  • List(ctx, search, briefRepresentation) ([]*Group, error) - List all groups
  • ListPaginated(ctx, search, briefRepresentation, first, max) ([]*Group, error) - Get paginated groups
  • ListWithSubGroups(ctx, searchQuery, briefRepresentation, first, max) ([]*Group, error) - List groups with subgroups included
  • ListWithParams(ctx, params) ([]*Group, error) - List groups with full parameter control
  • Count(ctx, search, top) (int, error) - Get total count of groups
  • GetByAttribute(ctx, attribute) (*Group, error) - Find group by attribute

Subgroup Operations

  • CreateSubGroup(ctx, groupID, name, attributes) (string, error) - Create a subgroup
  • ListSubGroups(ctx, groupID) ([]*Group, error) - Get all subgroups
  • ListSubGroupsPaginated(ctx, groupID, params) ([]*Group, error) - Get paginated subgroups with search
  • GetSubGroupByID(group, subGroupID) (*Group, error) - Find subgroup by ID
  • GetSubGroupByAttribute(group, attribute) (*Group, error) - Find subgroup by attribute

Important: Working with Subgroups

Keycloak API Behavior: Due to how Keycloak's REST API works, the SubGroups field is only populated in group responses when a search or q query parameter is provided. This is a limitation of Keycloak's API, not this library.

Two Approaches to Fetch Subgroups:

  1. Use ListWithSubGroups() (Recommended for hierarchies):

    // Fetches groups with their subgroups included in the response
    groups, err := client.Groups.ListWithSubGroups(ctx, "search-term", false, 0, 100)
    for _, group := range groups {
        if group.SubGroups != nil {
            for _, subgroup := range *group.SubGroups {
                fmt.Printf("Subgroup: %s\n", *subgroup.Name)
            }
        }
    }
  2. Use ListSubGroups() (Explicit subgroup fetch):

    // First get parent groups
    groups, err := client.Groups.List(ctx, nil, false)
    
    // Then explicitly fetch subgroups for each parent
    for _, group := range groups {
        subgroups, err := client.Groups.ListSubGroups(ctx, *group.ID)
        // Process subgroups...
    }

Note: The Get() method does NOT populate the SubGroups field. Use ListSubGroups() if you need to fetch children of a specific group.

Models

Group

type Group struct {
    ID          *string
    Name        *string
    Path        *string
    SubGroups   *[]*Group
    Attributes  *map[string][]string
    Access      *map[string]bool
    ClientRoles *map[string][]string
    RealmRoles  *[]string
}

GroupAttribute

type GroupAttribute struct {
    Key   string
    Value string
}

Error Handling

The package uses standard Go error handling with sentinel errors for common cases:

Sentinel Errors

The library provides typed errors for common scenarios:

  • keycloak.ErrGroupNotFound - Group not found in search or lookup operations
import "go.companyinfo.dev/keycloak"

group, err := client.Groups.GetByAttribute(ctx, attribute)
if err == keycloak.ErrGroupNotFound {
    log.Println("Group not found")
} else if err != nil {
    log.Fatalf("Unexpected error: %v", err)
}

HTTP Error Handling

// Check for specific HTTP status codes
if err != nil {
    if strings.Contains(err.Error(), "401") {
        // Authentication failed - check credentials
        log.Fatal("Authentication failed. Check your client credentials.")
    } else if strings.Contains(err.Error(), "403") {
        // Permission denied - check client roles
        log.Fatal("Permission denied. Ensure client has required roles.")
    } else if strings.Contains(err.Error(), "409") {
        // Conflict - resource already exists
        log.Println("Resource already exists")
    }
    return err
}

Best Error Handling Practices

// Always wrap errors with context
if err != nil {
    return fmt.Errorf("failed to create group %s: %w", groupName, err)
}

// Use context for timeout control
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

group, err := client.Groups.Get(ctx, groupID)
if err != nil {
    if ctx.Err() == context.DeadlineExceeded {
        return fmt.Errorf("operation timed out: %w", err)
    }
    return err
}

Best Practices

1. Always Use Context with Timeout

// ✅ Good: Prevents hanging requests
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// ❌ Bad: Can hang indefinitely
ctx := context.Background()

2. Configure Retry Logic for Production

// ✅ Good: Handles transient failures
client, err := keycloak.New(ctx, config,
    keycloak.WithRetry(3, 1*time.Second, 10*time.Second),
    keycloak.WithTimeout(30*time.Second),
)

// ❌ Bad: No retry, fails on first network hiccup
client, err := keycloak.New(ctx, config)

3. Use Pagination for Large Datasets

⚠️ Warning: Loading all groups at once can cause memory issues and timeouts in large Keycloak installations.

// ✅ Good: Memory efficient, handles any size
first := 0
max := 100
for {
    groups, err := client.Groups.ListPaginated(ctx, nil, false, first, max)
    if err != nil || len(groups) == 0 {
        break
    }
    processGroups(groups)
    first += max
}

// ❌ Bad: Loads everything into memory
groups, err := client.Groups.List(ctx, nil, false)

4. Store Secrets Securely

🔒 Security: Never commit credentials to version control. Use environment variables or secret management services.

// ✅ Good: Load from environment or secret manager
config := keycloak.Config{
    URL:          os.Getenv("KEYCLOAK_URL"),
    Realm:        os.Getenv("KEYCLOAK_REALM"),
    ClientID:     os.Getenv("KEYCLOAK_CLIENT_ID"),
    ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"),
}

// ❌ Bad: Hardcoded credentials
config := keycloak.Config{
    ClientSecret: "my-secret-12345", // Never do this!
}

5. Use Structured Logging

💡 Best Practice: Use structured logging (like slog) for better observability and easier debugging.

import "log/slog"

// ✅ Good: Structured logs with context
logger.InfoContext(ctx, "creating group",
    slog.String("name", groupName),
    slog.String("operation", "group.create"),
    slog.Any("attributes", attributes),
)

if err != nil {
    logger.ErrorContext(ctx, "failed to create group",
        slog.String("name", groupName),
        slog.String("error", err.Error()),
    )
}

// ❌ Bad: Unstructured logs (harder to parse and search)
log.Printf("Creating group %s with attributes %v", groupName, attributes)

6. Handle Idempotency

// ✅ Good: Check before create
func ensureGroupExists(ctx context.Context, client *keycloak.Client, name string) (string, error) {
    // Try to find existing
    groups, err := client.Groups.List(ctx, &name, false)
    if err != nil {
        return "", err
    }
    
    for _, g := range groups {
        if *g.Name == name {
            return *g.ID, nil
        }
    }
    
    // Create if not found
    return client.Groups.Create(ctx, name, nil)
}

7. Add Request Tracing

// ✅ Good: Add tracing headers
client, err := keycloak.New(ctx, config,
    keycloak.WithHeaders(map[string]string{
        "X-Request-ID": generateRequestID(),
        "X-Service":    "my-service",
    }),
)

8. Test with Mocks

// ✅ Good: Use interface for testing
type GroupManager interface {
    Create(ctx context.Context, name string, attrs map[string][]string) (string, error)
    Get(ctx context.Context, id string) (*keycloak.Group, error)
}

// Your service depends on interface, not concrete implementation
type MyService struct {
    groups GroupManager
}

Troubleshooting

Common Issues and Solutions

Authentication Failures (401 Unauthorized)

Problem: Client cannot authenticate with Keycloak.

Solutions:

  1. Verify credentials:

    # Test with curl
    curl -X POST "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token" \
      -d "client_id=admin-cli" \
      -d "client_secret=your-secret" \
      -d "grant_type=client_credentials"
  2. Check client configuration:

    • Client authentication: Must be ON
    • Service accounts enabled: Must be ON
    • Access Type: confidential
  3. Verify realm name: Case-sensitive!

Permission Denied (403 Forbidden)

Problem: Client authenticated but lacks permissions.

Solutions:

  1. Check service account roles:

    • Go to Clients → your client → Service accounts roles
    • Assign necessary roles: manage-groups, view-users, etc.
    • For full admin: assign realm-admin role
  2. Check fine-grained permissions: Some operations require specific permissions

Connection Timeouts

Problem: Requests hang or timeout.

Solutions:

  1. Add timeout to context:

    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
  2. Configure client timeout:

    client, err := keycloak.New(ctx, config,
        keycloak.WithTimeout(30*time.Second),
    )
  3. Check network connectivity:

    curl -v https://keycloak.example.com

Certificate Errors (TLS/SSL)

Problem: x509: certificate signed by unknown authority

🔒 Security Warning: Never use InsecureSkipVerify in production. It disables certificate validation and makes you vulnerable to man-in-the-middle attacks.

Solutions:

  1. For development only - Skip verification (NOT FOR PRODUCTION):

    import (
        "crypto/tls"
        "net/http"
    )
    
    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                InsecureSkipVerify: true, // ⚠️ NEVER in production!
            },
        },
    }
    client, err := keycloak.New(ctx, config,
        keycloak.WithHTTPClient(httpClient),
    )
  2. For production - Add CA certificate:

    import (
        "crypto/tls"
        "crypto/x509"
        "net/http"
        "os"
    )
    
    caCert, err := os.ReadFile("/path/to/ca.crt")
    if err != nil {
        return err
    }
    
    caCertPool := x509.NewCertPool()
    if !caCertPool.AppendCertsFromPEM(caCert) {
        return fmt.Errorf("failed to parse CA certificate")
    }
    
    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                RootCAs:    caCertPool,
                MinVersion: tls.VersionTLS12,
            },
        },
        Timeout: 30 * time.Second,
    }
    
    client, err := keycloak.New(ctx, config,
        keycloak.WithHTTPClient(httpClient),
    )

Group Not Found Errors

Problem: Cannot find groups that exist in Keycloak.

Solutions:

  1. Check search is case-sensitive: Use exact name
  2. Use briefRepresentation=false: Gets full group details including attributes
  3. Check group path: Groups might be subgroups

Rate Limiting (429 Too Many Requests)

Problem: Too many requests to Keycloak.

Solutions:

  1. Enable retry with backoff:

    client, err := keycloak.New(ctx, config,
        keycloak.WithRetry(5, 2*time.Second, 30*time.Second),
    )
  2. Implement request throttling: Use time.Ticker or rate limiter

  3. Batch operations: Use pagination instead of individual requests

Memory Issues with Large Datasets

Problem: Application runs out of memory when fetching many groups.

Solution: Always use pagination:

// ✅ Good: Process in chunks
first := 0
max := 100
for {
    groups, err := client.Groups.ListPaginated(ctx, nil, false, first, max)
    if err != nil || len(groups) == 0 {
        break
    }
    processGroups(groups) // Process and discard
    first += max
}

Debug Mode

Enable debug logging to see all requests and responses:

client, err := keycloak.New(ctx, config,
    keycloak.WithDebug(true),
)

FAQ

General Questions

Q: Is this library production-ready?
A: Yes. It includes automatic token refresh, retry logic, comprehensive error handling, and has been battle-tested in production environments.

Q: What versions of Keycloak are supported?
A: Keycloak 26.x and later. The library is tested against Keycloak 26.

Q: Does it support Keycloak 25 or earlier?
A: It may work, but it's not officially tested.

Q: Can I use this with Red Hat SSO?
A: Yes, Red Hat SSO is based on Keycloak, so this library should work.

Authentication

Q: What authentication methods are supported?
A: Currently only OAuth2 client credentials flow (service accounts). This is the recommended method for server-to-server communication.

Q: Can I use username/password authentication?
A: Not currently. Client credentials flow is more secure for automated processes.

Q: How often do tokens refresh?
A: Tokens are automatically refreshed before expiration. You don't need to handle this.

Feature Support

Q: Does this support user management?
A: Not yet. Currently only group management is implemented. User management is planned for a future release.

Q: Can I manage roles?
A: Not yet. Role management is planned for a future release.

Q: What about realm management?
A: Not currently. The library focuses on resource management within a realm.

Q: Can I create custom attributes?
A: Yes! Groups support arbitrary attributes as map[string][]string.

Performance

Q: How many requests per second can it handle?
A: This depends on your Keycloak instance. The library includes retry logic and supports concurrent requests.

Q: Should I create one client per request or reuse it?
A: Always reuse the client. Create one client at application startup and reuse it throughout your application's lifetime. The client:

  • Maintains HTTP connection pools (expensive to recreate)
  • Caches OAuth2 access tokens (avoids repeated authentication)
  • Is safe for concurrent use across goroutines
  • Creating new clients for each request wastes resources and degrades performance
// ✅ Correct: Single client, initialized once
var client *keycloak.Client

func main() {
    var err error
    client, err = keycloak.New(context.Background(), config)
    // ... use client throughout application lifecycle
}

Q: Does it support connection pooling?
A: Yes, through the underlying http.Client. You can customize this with WithHTTPClient().

Testing and Development

Q: How do I test code that uses this library?
A: The library uses interfaces (GroupsClient, etc.) that you can mock. See the test files for examples.

Q: Can I run tests without a real Keycloak instance?
A: Yes. Unit tests and mock suite tests don't require Keycloak. Only integration tests need a real instance.

Q: Should I test against production Keycloak?
A: Never! Always use a dedicated test realm. Integration tests can create/delete resources.

Errors and Edge Cases

Q: What happens if Keycloak is down?
A: Requests will fail after the configured timeout and retry attempts. Use appropriate error handling and monitoring.

Q: Are operations atomic?
A: No. Keycloak API calls are independent. If you need transaction-like behavior, implement compensating operations (see Example 2 above).

Q: What if I create duplicate groups?
A: Keycloak allows groups with the same name. Use attributes (like externalID) to enforce uniqueness in your application.

Configuration Options

Q: How do I use a proxy?
A: Use WithProxy(proxyURL) option when creating the client.

Q: Can I customize HTTP headers?
A: Yes, use WithHeaders(map[string]string{...}) for headers on all requests.

Q: What's the default timeout?
A: The client itself doesn't set a default timeout, so requests will use Go's standard HTTP client behavior (no timeout). Always set an explicit timeout using WithTimeout() when creating the client or use context.WithTimeout() for individual operations. This prevents operations from hanging indefinitely.

// Recommended: Set timeout when creating client
client, err := keycloak.New(ctx, config,
    keycloak.WithTimeout(30*time.Second),
)

// Or use context timeout for specific operations
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
groups, err := client.Groups.List(ctx, nil, false)

Architecture

The package follows a resource-based client design pattern:

  • Main Client: Entry point that holds resource-specific clients
  • Resource Clients: Focused interfaces for each Keycloak resource (Groups, Users, Roles, etc.)
  • Shared State: Authentication and configuration shared across all resource clients

This design makes it easy to add new Keycloak resources without bloating a single interface, and allows for better organization and testability.

Performance Characteristics

Understanding the performance profile helps you optimize your application:

Connection Management:

  • HTTP connection pooling automatically managed by Go's http.Client
  • Keep-alive connections reused across requests
  • Default pool size: 100 idle connections, 10 per host

Authentication:

  • OAuth2 access tokens cached automatically
  • Token refresh handled transparently before expiration
  • Minimal authentication overhead after initial setup

Memory Usage:

  • Client baseline: ~2-5 MB (includes HTTP client, configuration, token cache)
  • Per group object: ~1-2 KB (varies with attributes and subgroups)
  • 10,000 groups in memory: ~10-20 MB
  • Use pagination to control memory footprint

Concurrency:

  • Thread-safe: Safe for concurrent use across multiple goroutines
  • No locking in read paths (immutable after initialization)
  • Recommended: Create one client at startup, reuse across application

Best Practices for Performance:

// ✅ Good: Single client instance, reused everywhere
var keycloakClient *keycloak.Client

func init() {
    var err error
    keycloakClient, err = keycloak.New(context.Background(), config)
    // handle error
}

// ❌ Bad: Creating new client for each request (wastes resources)
func handleRequest() {
    client, _ := keycloak.New(context.Background(), config) // Don't do this!
}

Throughput Considerations:

  • Bottleneck is typically Keycloak server, not this client
  • Client can handle hundreds of concurrent requests
  • Use context timeouts to prevent slow requests from blocking
  • Consider rate limiting for bulk operations

Configuration

Required Configuration

The Config struct requires the following fields:

Field Type Required Description
URL string Keycloak server URL (e.g., https://keycloak.example.com)
Realm string Keycloak realm name
ClientID string OAuth2 client ID
ClientSecret string OAuth2 client secret

Example:

config := keycloak.Config{
    URL:          "https://keycloak.example.com",
    Realm:        "production",
    ClientID:     "backend-service",
    ClientSecret: os.Getenv("KEYCLOAK_CLIENT_SECRET"),
}

Environment-Based Configuration

Best practice: Load configuration from environment variables:

import (
    "os"
    "log"
)

func loadConfig() keycloak.Config {
    config := keycloak.Config{
        URL:          getEnv("KEYCLOAK_URL", ""),
        Realm:        getEnv("KEYCLOAK_REALM", ""),
        ClientID:     getEnv("KEYCLOAK_CLIENT_ID", ""),
        ClientSecret: getEnv("KEYCLOAK_CLIENT_SECRET", ""),
    }
    
    // Validate required fields
    if config.URL == "" || config.Realm == "" || 
       config.ClientID == "" || config.ClientSecret == "" {
        log.Fatal("Missing required Keycloak configuration")
    }
    
    return config
}

func getEnv(key, fallback string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return fallback
}

Environment Variables:

export KEYCLOAK_URL="https://keycloak.example.com"
export KEYCLOAK_REALM="production"
export KEYCLOAK_CLIENT_ID="backend-service"
export KEYCLOAK_CLIENT_SECRET="your-secret-here"

Optional Configuration

Use functional options to customize client behavior:

Option Description Default Recommendation
WithPageSize(size int) Default page size for pagination 50 Use 100-500 for batch operations
WithTimeout(duration) Request timeout for API calls No timeout Always set (e.g., 30s)
WithRetry(count, wait, maxWait) Retry behavior with exponential backoff No retry Use 3-5 retries for production
WithDebug(bool) Enable debug logging false Only in development
WithHeaders(map[string]string) Add custom headers None Use for tracing/correlation IDs
WithUserAgent(string) Set custom User-Agent "" Include app name/version
WithProxy(proxyURL) Configure HTTP proxy None As needed for your network
WithHTTPClient(*http.Client) Use custom HTTP client Default For advanced scenarios only

Recommended Production Configuration

client, err := keycloak.New(ctx, config,
    // Essential for production
    keycloak.WithTimeout(30*time.Second),                    // Prevent hanging
    keycloak.WithRetry(3, 1*time.Second, 10*time.Second),   // Handle transient failures
    
    // Recommended for operations and debugging
    keycloak.WithUserAgent(fmt.Sprintf("myapp/%s", version)), // Identify your app
    keycloak.WithHeaders(map[string]string{
        "X-Service": "backend-api",                             // For tracking
    }),
    
    // Optional based on your needs
    keycloak.WithPageSize(100),                              // For bulk operations
)

Advanced HTTP Client Configuration

For custom TLS, proxy, or connection pooling:

import (
    "crypto/tls"
    "net/http"
    "time"
)

// Custom HTTP client with connection pooling
httpClient := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
        TLSClientConfig: &tls.Config{
            MinVersion: tls.VersionTLS12,
        },
    },
    Timeout: 30 * time.Second,
}

client, err := keycloak.New(ctx, config,
    keycloak.WithHTTPClient(httpClient),
)

See the Advanced Configuration section for more usage examples.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

What This Means

  • Commercial use - Use in commercial products
  • Modification - Modify the source code
  • Distribution - Distribute the library
  • Patent use - Patent grant included
  • ⚠️ Trademark - No trademark rights granted
  • ⚠️ Liability - No warranty provided
  • ⚠️ State changes - Must document modifications

Testing

The project includes comprehensive test coverage with multiple test types:

Test Structure

  • Unit Tests (*_test.go) - Test individual components without external dependencies
  • Mock Suite Tests (groups_mock_suite_test.go) - Test API operations using HTTP mocks
  • Integration Tests (groups_integration_suite_test.go) - Test against a real Keycloak instance

Running Tests

Run Unit Tests Only

# Fast unit tests with mocks
go test -v ./...

# Or with short flag to skip integration tests
go test -v -short ./...

Run Mock Suite Tests

# Run just the mock suite
go test -v -run TestGroupsMockSuite ./...

Run Integration Tests

Integration tests require a running Keycloak instance. Set up your environment first:

⚠️ CRITICAL WARNING: Integration tests create and delete resources. NEVER run them against production! Always use a dedicated test realm.

# Copy the example environment file
cp .env.example .env

# Edit .env with your Keycloak credentials
# Then run integration tests
go test -v -tags=integration ./...

Required environment variables for integration tests:

  • KEYCLOAK_URL - Keycloak server URL (e.g., http://localhost:8080)
  • KEYCLOAK_REALM - Test realm name (use a dedicated test realm!)
  • KEYCLOAK_CLIENT_ID - Client ID with admin privileges
  • KEYCLOAK_CLIENT_SECRET - Client secret

💡 Tip: Use Docker to run a local Keycloak instance for testing. See setup instructions below.

Run All Tests

# Run all tests including integration tests
go test -v -tags=integration ./...

Test Coverage

Generate and view test coverage:

# Generate coverage report
go test -v -cover -coverprofile=coverage.out ./...

# View coverage in browser
go tool cover -html=coverage.out

# Or view in terminal
go tool cover -func=coverage.out

Setting Up Keycloak for Integration Tests

  1. Run Keycloak locally (using Docker):

    docker run -d \
      --name keycloak-test \
      -p 8080:8080 \
      -e KEYCLOAK_ADMIN=admin \
      -e KEYCLOAK_ADMIN_PASSWORD=admin \
      quay.io/keycloak/keycloak:latest \
      start-dev
  2. Create a test realm:

  3. Create a client for testing:

    • Go to Clients → Create
    • Client ID: admin-cli (or custom name)
    • Client authentication: ON
    • Service accounts enabled: ON
    • Save
  4. Assign admin roles:

    • Go to Clients → your client → Service accounts roles
    • Assign Client role: realm-admin (or at minimum manage-groups)
  5. Get credentials:

    • Go to Clients → your client → Credentials
    • Copy the client secret
  6. Update .env file:

    KEYCLOAK_URL=http://localhost:8080
    KEYCLOAK_REALM=test-realm
    KEYCLOAK_CLIENT_ID=admin-cli
    KEYCLOAK_CLIENT_SECRET=your-copied-secret

Writing Tests

The project uses testify/suite for organized test suites and testify/assert for readable assertions.

Example Unit Test

func TestWithPageSize(t *testing.T) {
    tests := []struct {
        name      string
        size      int
        wantErr   bool
        wantValue int
    }{
        {
            name:      "valid page size",
            size:      100,
            wantErr:   false,
            wantValue: 100,
        },
        {
            name:    "invalid page size",
            size:    -1,
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            client := &Client{pageSize: defaultSize}
            err := WithPageSize(tt.size)(client)

            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
                assert.Equal(t, tt.wantValue, client.pageSize)
            }
        })
    }
}

Example Mock Suite Test

func (s *GroupsMockSuite) TestGetGroupSuccess() {
    groupID := "test-group-id"
    expectedGroup := &Group{
        ID:   StringP(groupID),
        Name: StringP("Test Group"),
    }

    path := fmt.Sprintf("/admin/realms/%s/groups/%s", s.mockRealm, groupID)
    s.mockJSONResponse(http.MethodGet, path, http.StatusOK, expectedGroup)

    group, err := s.client.Groups.Get(s.ctx, groupID)
    
    s.NoError(err)
    s.NotNil(group)
    s.Equal(*expectedGroup.ID, *group.ID)
}

Example Integration Test

func (s *GroupsIntegrationTestSuite) TestGroupLifecycle() {
    // Create
    groupID, err := s.client.Groups.Create(s.ctx, "Test Group", nil)
    s.Require().NoError(err)
    s.trackGroup(groupID) // Auto-cleanup

    // Read
    group, err := s.client.Groups.Get(s.ctx, groupID)
    s.NoError(err)
    s.Equal("Test Group", *group.Name)

    // Update
    group.Description = keycloak.StringP("Updated")
    err = s.client.Groups.Update(s.ctx, *group)
    s.NoError(err)

    // Delete
    err = s.client.Groups.Delete(s.ctx, groupID)
    s.NoError(err)
}

Continuous Integration

The tests are designed to run in CI/CD pipelines:

# Example GitHub Actions workflow
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.24'
      
      # Run unit and mock tests
      - name: Unit Tests
        run: go test -v -short -cover ./...
      
      # Optional: Run integration tests if Keycloak is available
      - name: Integration Tests
        env:
          KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }}
          KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }}
          KEYCLOAK_CLIENT_ID: ${{ secrets.KEYCLOAK_CLIENT_ID }}
          KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}
        run: go test -v -tags=integration ./...
        if: env.KEYCLOAK_URL != ''

Contributing

Contributions are welcome! We appreciate your help in making this library better.

How to Contribute

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/your-feature-name
  3. Make your changes with clear, focused commits
  4. Add tests for new functionality
  5. Run tests: go test -v ./...
  6. Update documentation if needed
  7. Submit a pull request

Code Standards

  • Follow Effective Go guidelines
  • Use gofmt to format your code
  • Add meaningful comments for exported functions
  • Keep functions small and focused
  • Write tests for all new functionality
  • Maintain backward compatibility when possible

Testing Requirements

All contributions must include appropriate tests:

  • Unit tests for new functions
  • Mock tests for API interactions
  • Integration tests for critical paths (when applicable)

Run all tests before submitting:

# Unit and mock tests
go test -v -short ./...

# With coverage
go test -v -cover -coverprofile=coverage.out ./...

# Integration tests (requires Keycloak)
go test -v -tags=integration ./...

What We're Looking For

High Priority:

  • Bug fixes with test cases
  • Performance improvements
  • Documentation improvements
  • Additional resource clients (Users, Roles, etc.)

Welcome:

  • New features with clear use cases
  • Code quality improvements
  • Example applications

Please Discuss First:

  • Breaking API changes
  • Major architectural changes
  • Large new features

Reporting Issues

When reporting bugs, please include:

  1. Go version: go version
  2. Keycloak version
  3. Library version
  4. Minimal reproduction code
  5. Expected vs actual behavior
  6. Error messages (with stack traces if applicable)

Questions and Support

  • Documentation issues: Open an issue with the "documentation" label
  • Usage questions: Check the FAQ first, then open a discussion
  • Bug reports: Open an issue with reproduction steps
  • Feature requests: Open an issue describing the use case

Development Setup

  1. Clone the repository:

    git clone https://github.com/companyinfo/keycloak.git
    cd keycloak
  2. Install dependencies:

    go mod download
  3. Run tests:

    go test -v ./...
  4. (Optional) Set up Keycloak for integration tests:

    # See Testing section for detailed setup
    cp .env.example .env
    # Edit .env with your test Keycloak credentials

Code Review Process

  1. All submissions require review
  2. We aim to review PRs within 3-5 business days
  3. Address feedback promptly
  4. Once approved, maintainers will merge

Thank you for contributing! 🙏

Acknowledgments

Built with:

Inspired by the Keycloak community and Go best practices.


Maintained by CompanyInfo

Found this useful? Give it a star ⭐ to show your support!

About

Keycloak is an idiomatic Go client for the Keycloak Admin API

Topics

Resources

License

Stars

Watchers

Forks