An open API service indexing awesome lists of open source software.

https://github.com/companyinfo/keycloak

Keycloak is an idiomatic Go client for the Keycloak Admin API
https://github.com/companyinfo/keycloak

go golang keycloak package pkg

Last synced: 30 days ago
JSON representation

Keycloak is an idiomatic Go client for the Keycloak Admin API

Awesome Lists containing this project

README

          

# Keycloak Go Client

[![CI](https://github.com/companyinfo/keycloak/actions/workflows/ci.yaml/badge.svg)](https://github.com/companyinfo/keycloak/actions/workflows/ci.yaml)
[![codecov](https://codecov.io/gh/companyinfo/keycloak/branch/main/graph/badge.svg)](https://codecov.io/gh/companyinfo/keycloak)
[![Go Reference](https://pkg.go.dev/badge/go.companyinfo.dev/keycloak.svg)](https://pkg.go.dev/go.companyinfo.dev/keycloak)
[![Go Report Card](https://goreportcard.com/badge/go.companyinfo.dev/keycloak)](https://goreportcard.com/report/go.companyinfo.dev/keycloak)
[![Go Version](https://img.shields.io/badge/Go-1.24+-blue)](https://go.dev/dl/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)

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

**Quick Links**: [Installation](#installation) | [Quick Start](#quick-start) | [Examples](examples/) | [API Docs](https://pkg.go.dev/go.companyinfo.dev/keycloak)

## 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](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage](#usage)
- [Basic Setup](#basic-setup)
- [Advanced Configuration](#advanced-configuration-with-options)
- [Group Operations](#creating-a-group)
- [Real-World Examples](#real-world-examples)
- [API Reference](#api-reference)
- [Error Handling](#error-handling)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [Testing](#testing)
- [Contributing](#contributing)
- [FAQ](#faq)
- [License](#license)

## 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](#setting-up-keycloak-for-integration-tests) for detailed configuration steps.

## Installation

```bash
go get go.companyinfo.dev/keycloak
```

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

```go
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:

```go
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

```go
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:

```go
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

```go
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

```go
// 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

```go
// 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

```go
// 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

```go
// 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

```go
// 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

```go
// 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

```go
// 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

```go
// 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:

```go
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)**:

```go
// 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)**:

```go
// 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

```go
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

```go
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

```go
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

```go
// 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

```go
// 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

```go
// ✅ 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

```go
// ✅ 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.

```go
// ✅ 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.

```go
// ✅ 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.

```go
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

```go
// ✅ 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

```go
// ✅ 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

```go
// ✅ 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**:

```bash
# 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**:

```go
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
```

2. **Configure client timeout**:

```go
client, err := keycloak.New(ctx, config,
keycloak.WithTimeout(30*time.Second),
)
```

3. **Check network connectivity**:

```bash
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):

```go
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:

```go
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**:

```go
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:

```go
// ✅ 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:

```go
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

```go
// ✅ 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.

```go
// 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**:

```go
// ✅ 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**:

```go
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:

```go
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**:

```bash
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

```go
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:

```go
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](#advanced-configuration-with-options) section for more usage examples.

## License

This project is licensed under the Apache License 2.0 - see the [LICENSE](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

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

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

#### Run Mock Suite Tests

```bash
# 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.

```bash
# 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

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

### Test Coverage

Generate and view test coverage:

```bash
# 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):

```bash
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:**
- Access Keycloak Admin Console at
- Create a new realm (e.g., `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:**

```env
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](https://github.com/stretchr/testify#suite-package) for organized test suites and [testify/assert](https://github.com/stretchr/testify#assert-package) for readable assertions.

#### Example Unit Test

```go
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

```go
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

```go
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:

```yaml
# 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](https://golang.org/doc/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:

```bash
# 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](#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:

```bash
git clone https://github.com/companyinfo/keycloak.git
cd keycloak
```

2. Install dependencies:

```bash
go mod download
```

3. Run tests:

```bash
go test -v ./...
```

4. (Optional) Set up Keycloak for integration tests:

```bash
# 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:

- [go-resty](https://github.com/go-resty/resty) - HTTP client library
- [testify](https://github.com/stretchr/testify) - Testing toolkit

Inspired by the Keycloak community and Go best practices.

---

**Maintained by** [CompanyInfo](https://github.com/companyinfo)

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