Golang Integration

Golang Integration

This guide covers how to integrate Skycloak authentication into Go applications using various middleware options and best practices for different frameworks.

Prerequisites

  • Go 1.18+
  • Skycloak cluster with configured realm and client
  • Basic understanding of Go HTTP handlers and middleware

Quick Start

1. Install Dependencies

Using go-oidc (recommended):

go get github.com/coreos/go-oidc/v3/oidc
go get golang.org/x/oauth2

Or using gocloak for Keycloak-specific features:

go get github.com/Nerzal/gocloak/v13

2. Basic OAuth2 Configuration

// config/auth.go
package config

import (
    "context"
    "github.com/coreos/go-oidc/v3/oidc"
    "golang.org/x/oauth2"
)

type AuthConfig struct {
    Provider     *oidc.Provider
    OAuth2Config oauth2.Config
    Verifier     *oidc.IDTokenVerifier
}

func NewAuthConfig(ctx context.Context) (*AuthConfig, error) {
    // Initialize OIDC provider
    provider, err := oidc.NewProvider(ctx, "https://your-cluster-id.app.skycloak.io/realms/your-realm")
    if err != nil {
        return nil, err
    }

    // Configure OAuth2
    oauth2Config := oauth2.Config{
        ClientID:     "your-client-id",
        ClientSecret: "your-client-secret",
        RedirectURL:  "http://localhost:8080/callback",
        Endpoint:     provider.Endpoint(),
        Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
    }

    // Create ID token verifier
    verifier := provider.Verifier(&oidc.Config{
        ClientID: oauth2Config.ClientID,
    })

    return &AuthConfig{
        Provider:     provider,
        OAuth2Config: oauth2Config,
        Verifier:     verifier,
    }, nil
}

3. HTTP Handlers

// handlers/auth.go
package handlers

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "net/http"
    "time"
)

type AuthHandler struct {
    config *config.AuthConfig
}

func NewAuthHandler(config *config.AuthConfig) *AuthHandler {
    return &AuthHandler{config: config}
}

// Login initiates the OAuth2 flow
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
    // Generate state for CSRF protection
    state, err := generateRandomState()
    if err != nil {
        http.Error(w, "Failed to generate state", http.StatusInternalServerError)
        return
    }

    // Store state in cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "oauth_state",
        Value:    state,
        MaxAge:   300, // 5 minutes
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
    })

    // Redirect to authorization endpoint
    authURL := h.config.OAuth2Config.AuthCodeURL(state)
    http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
}

// Callback handles the OAuth2 callback
func (h *AuthHandler) Callback(w http.ResponseWriter, r *http.Request) {
    // Verify state
    stateCookie, err := r.Cookie("oauth_state")
    if err != nil {
        http.Error(w, "State cookie not found", http.StatusBadRequest)
        return
    }

    if r.URL.Query().Get("state") != stateCookie.Value {
        http.Error(w, "Invalid state", http.StatusBadRequest)
        return
    }

    // Exchange code for token
    code := r.URL.Query().Get("code")
    token, err := h.config.OAuth2Config.Exchange(r.Context(), code)
    if err != nil {
        http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
        return
    }

    // Extract and verify ID token
    rawIDToken, ok := token.Extra("id_token").(string)
    if !ok {
        http.Error(w, "No ID token found", http.StatusInternalServerError)
        return
    }

    idToken, err := h.config.Verifier.Verify(r.Context(), rawIDToken)
    if err != nil {
        http.Error(w, "Failed to verify ID token", http.StatusInternalServerError)
        return
    }

    // Create session
    session := &Session{
        AccessToken:  token.AccessToken,
        RefreshToken: token.RefreshToken,
        IDToken:      rawIDToken,
        Expiry:       token.Expiry,
    }

    // Store session (implementation depends on your session store)
    sessionID, err := storeSession(session)
    if err != nil {
        http.Error(w, "Failed to create session", http.StatusInternalServerError)
        return
    }

    // Set session cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    sessionID,
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
        MaxAge:   86400, // 24 hours
    })

    // Redirect to dashboard
    http.Redirect(w, r, "/dashboard", http.StatusTemporaryRedirect)
}

// Logout handles user logout
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
    // Clear session
    sessionCookie, err := r.Cookie("session_id")
    if err == nil {
        deleteSession(sessionCookie.Value)
    }

    // Clear session cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    "",
        MaxAge:   -1,
        HttpOnly: true,
        Secure:   true,
    })

    // Redirect to Keycloak logout
    logoutURL := fmt.Sprintf("%s/protocol/openid-connect/logout?redirect_uri=%s",
        h.config.Provider.Endpoint().AuthURL[:strings.LastIndex(h.config.Provider.Endpoint().AuthURL, "/protocol")],
        url.QueryEscape("http://localhost:8080"),
    )
    
    http.Redirect(w, r, logoutURL, http.StatusTemporaryRedirect)
}

func generateRandomState() (string, error) {
    b := make([]byte, 32)
    _, err := rand.Read(b)
    if err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(b), nil
}

4. Authentication Middleware

// middleware/auth.go
package middleware

import (
    "context"
    "net/http"
    "strings"
)

type contextKey string

const (
    UserContextKey contextKey = "user"
)

type User struct {
    ID       string
    Username string
    Email    string
    Roles    []string
    Groups   []string
}

// RequireAuth middleware ensures user is authenticated
func RequireAuth(config *config.AuthConfig) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Check session cookie
            sessionCookie, err := r.Cookie("session_id")
            if err != nil {
                http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
                return
            }

            // Retrieve session
            session, err := getSession(sessionCookie.Value)
            if err != nil {
                http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
                return
            }

            // Check if token needs refresh
            if time.Until(session.Expiry) < 5*time.Minute {
                newToken, err := refreshToken(config, session.RefreshToken)
                if err != nil {
                    http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
                    return
                }
                session.AccessToken = newToken.AccessToken
                session.Expiry = newToken.Expiry
                updateSession(sessionCookie.Value, session)
            }

            // Parse ID token for user info
            idToken, err := config.Verifier.Verify(r.Context(), session.IDToken)
            if err != nil {
                http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
                return
            }

            // Extract user claims
            var claims struct {
                Sub               string   `json:"sub"`
                PreferredUsername string   `json:"preferred_username"`
                Email             string   `json:"email"`
                RealmAccess       struct {
                    Roles []string `json:"roles"`
                } `json:"realm_access"`
                Groups []string `json:"groups"`
            }
            
            if err := idToken.Claims(&claims); err != nil {
                http.Error(w, "Failed to parse claims", http.StatusInternalServerError)
                return
            }

            // Create user object
            user := &User{
                ID:       claims.Sub,
                Username: claims.PreferredUsername,
                Email:    claims.Email,
                Roles:    claims.RealmAccess.Roles,
                Groups:   claims.Groups,
            }

            // Add user to context
            ctx := context.WithValue(r.Context(), UserContextKey, user)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// RequireRole middleware ensures user has specific role
func RequireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user, ok := r.Context().Value(UserContextKey).(*User)
            if !ok {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }

            hasRole := false
            for _, userRole := range user.Roles {
                if userRole == role {
                    hasRole = true
                    break
                }
            }

            if !hasRole {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// GetUser retrieves user from context
func GetUser(ctx context.Context) (*User, bool) {
    user, ok := ctx.Value(UserContextKey).(*User)
    return user, ok
}

Framework Integration

Gin Framework

// middleware/gin_auth.go
package middleware

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

// GinAuthMiddleware for Gin framework
func GinAuthMiddleware(config *config.AuthConfig) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Get token from Authorization header
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization header"})
            c.Abort()
            return
        }

        // Extract bearer token
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header"})
            c.Abort()
            return
        }

        // Verify token
        token, err := verifyAccessToken(config, parts[1])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        // Extract claims
        var claims map[string]interface{}
        if err := token.Claims(&claims); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse claims"})
            c.Abort()
            return
        }

        // Set user in context
        c.Set("user", claims)
        c.Next()
    }
}

// GinRequireRole middleware
func GinRequireRole(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        user, exists := c.Get("user")
        if !exists {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
            c.Abort()
            return
        }

        claims := user.(map[string]interface{})
        realmAccess, ok := claims["realm_access"].(map[string]interface{})
        if !ok {
            c.JSON(http.StatusForbidden, gin.H{"error": "No realm access"})
            c.Abort()
            return
        }

        roles, ok := realmAccess["roles"].([]interface{})
        if !ok {
            c.JSON(http.StatusForbidden, gin.H{"error": "No roles found"})
            c.Abort()
            return
        }

        hasRole := false
        for _, r := range roles {
            if r.(string) == role {
                hasRole = true
                break
            }
        }

        if !hasRole {
            c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
            c.Abort()
            return
        }

        c.Next()
    }
}

// Example Gin routes
func SetupGinRoutes(router *gin.Engine, config *config.AuthConfig) {
    // Public routes
    router.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "healthy"})
    })

    // Protected routes
    protected := router.Group("/api")
    protected.Use(GinAuthMiddleware(config))
    {
        protected.GET("/profile", func(c *gin.Context) {
            user, _ := c.Get("user")
            c.JSON(http.StatusOK, user)
        })

        // Admin only routes
        admin := protected.Group("/admin")
        admin.Use(GinRequireRole("admin"))
        {
            admin.GET("/users", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{"users": []string{"user1", "user2"}})
            })
        }
    }
}

Echo Framework

// middleware/echo_auth.go
package middleware

import (
    "github.com/labstack/echo/v4"
    "net/http"
)

// EchoAuthMiddleware for Echo framework
func EchoAuthMiddleware(config *config.AuthConfig) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Get token from Authorization header
            authHeader := c.Request().Header.Get("Authorization")
            if authHeader == "" {
                return echo.NewHTTPError(http.StatusUnauthorized, "Missing authorization header")
            }

            // Extract bearer token
            parts := strings.Split(authHeader, " ")
            if len(parts) != 2 || parts[0] != "Bearer" {
                return echo.NewHTTPError(http.StatusUnauthorized, "Invalid authorization header")
            }

            // Verify token
            token, err := verifyAccessToken(config, parts[1])
            if err != nil {
                return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
            }

            // Extract claims
            var claims map[string]interface{}
            if err := token.Claims(&claims); err != nil {
                return echo.NewHTTPError(http.StatusInternalServerError, "Failed to parse claims")
            }

            // Set user in context
            c.Set("user", claims)
            return next(c)
        }
    }
}

// Example Echo routes
func SetupEchoRoutes(e *echo.Echo, config *config.AuthConfig) {
    // Public routes
    e.GET("/health", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{"status": "healthy"})
    })

    // Protected routes
    api := e.Group("/api")
    api.Use(EchoAuthMiddleware(config))
    
    api.GET("/profile", func(c echo.Context) error {
        user := c.Get("user")
        return c.JSON(http.StatusOK, user)
    })
}

Service-to-Service Authentication

Client Credentials Flow

// services/auth_client.go
package services

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "strings"
    "sync"
    "time"
)

type ServiceAuthClient struct {
    tokenURL     string
    clientID     string
    clientSecret string
    httpClient   *http.Client
    
    mu          sync.RWMutex
    accessToken string
    expiry      time.Time
}

func NewServiceAuthClient(tokenURL, clientID, clientSecret string) *ServiceAuthClient {
    return &ServiceAuthClient{
        tokenURL:     tokenURL,
        clientID:     clientID,
        clientSecret: clientSecret,
        httpClient:   &http.Client{Timeout: 10 * time.Second},
    }
}

// GetToken retrieves a valid access token, refreshing if necessary
func (c *ServiceAuthClient) GetToken(ctx context.Context) (string, error) {
    c.mu.RLock()
    if c.accessToken != "" && time.Now().Before(c.expiry.Add(-30*time.Second)) {
        token := c.accessToken
        c.mu.RUnlock()
        return token, nil
    }
    c.mu.RUnlock()

    // Need to refresh token
    c.mu.Lock()
    defer c.mu.Unlock()

    // Double-check after acquiring write lock
    if c.accessToken != "" && time.Now().Before(c.expiry.Add(-30*time.Second)) {
        return c.accessToken, nil
    }

    // Request new token
    data := url.Values{}
    data.Set("grant_type", "client_credentials")
    data.Set("client_id", c.clientID)
    data.Set("client_secret", c.clientSecret)

    req, err := http.NewRequestWithContext(ctx, "POST", c.tokenURL, strings.NewReader(data.Encode()))
    if err != nil {
        return "", err
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("token request failed with status: %d", resp.StatusCode)
    }

    var tokenResp struct {
        AccessToken string `json:"access_token"`
        ExpiresIn   int    `json:"expires_in"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
        return "", err
    }

    c.accessToken = tokenResp.AccessToken
    c.expiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)

    return c.accessToken, nil
}

// AuthenticatedClient returns an HTTP client with automatic token injection
func (c *ServiceAuthClient) AuthenticatedClient() *http.Client {
    return &http.Client{
        Transport: &AuthTransport{
            Base:       http.DefaultTransport,
            AuthClient: c,
        },
        Timeout: 30 * time.Second,
    }
}

type AuthTransport struct {
    Base       http.RoundTripper
    AuthClient *ServiceAuthClient
}

func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    token, err := t.AuthClient.GetToken(req.Context())
    if err != nil {
        return nil, err
    }

    req.Header.Set("Authorization", "Bearer "+token)
    return t.Base.RoundTrip(req)
}

JWT Validation

Custom JWT Validator

// auth/jwt_validator.go
package auth

import (
    "crypto/rsa"
    "encoding/json"
    "errors"
    "fmt"
    "github.com/golang-jwt/jwt/v4"
    "net/http"
    "sync"
    "time"
)

type JWTValidator struct {
    jwksURL    string
    clientID   string
    httpClient *http.Client
    
    mu       sync.RWMutex
    keyCache map[string]*rsa.PublicKey
}

func NewJWTValidator(jwksURL, clientID string) *JWTValidator {
    return &JWTValidator{
        jwksURL:    jwksURL,
        clientID:   clientID,
        httpClient: &http.Client{Timeout: 10 * time.Second},
        keyCache:   make(map[string]*rsa.PublicKey),
    }
}

// ValidateToken validates a JWT token
func (v *JWTValidator) ValidateToken(tokenString string) (*jwt.Token, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Verify signing method
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }

        // Get key ID
        kid, ok := token.Header["kid"].(string)
        if !ok {
            return nil, errors.New("kid not found in token header")
        }

        // Get public key
        return v.getPublicKey(kid)
    })

    if err != nil {
        return nil, err
    }

    // Validate claims
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, errors.New("invalid claims format")
    }

    // Verify audience
    if !v.verifyAudience(claims) {
        return nil, errors.New("invalid audience")
    }

    // Verify expiration
    if !v.verifyExpiration(claims) {
        return nil, errors.New("token expired")
    }

    return token, nil
}

func (v *JWTValidator) getPublicKey(kid string) (*rsa.PublicKey, error) {
    // Check cache
    v.mu.RLock()
    if key, ok := v.keyCache[kid]; ok {
        v.mu.RUnlock()
        return key, nil
    }
    v.mu.RUnlock()

    // Fetch JWKS
    resp, err := v.httpClient.Get(v.jwksURL)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var jwks struct {
        Keys []json.RawMessage `json:"keys"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
        return nil, err
    }

    // Parse keys and update cache
    v.mu.Lock()
    defer v.mu.Unlock()

    for _, keyData := range jwks.Keys {
        key, err := jwt.ParseRSAPublicKeyFromPEM(keyData)
        if err != nil {
            continue
        }
        
        var keyInfo struct {
            Kid string `json:"kid"`
        }
        if err := json.Unmarshal(keyData, &keyInfo); err != nil {
            continue
        }
        
        v.keyCache[keyInfo.Kid] = key
    }

    if key, ok := v.keyCache[kid]; ok {
        return key, nil
    }

    return nil, errors.New("key not found")
}

func (v *JWTValidator) verifyAudience(claims jwt.MapClaims) bool {
    aud, ok := claims["aud"]
    if !ok {
        return false
    }

    switch aud := aud.(type) {
    case string:
        return aud == v.clientID
    case []interface{}:
        for _, a := range aud {
            if a == v.clientID {
                return true
            }
        }
    }
    
    return false
}

func (v *JWTValidator) verifyExpiration(claims jwt.MapClaims) bool {
    exp, ok := claims["exp"].(float64)
    if !ok {
        return false
    }
    
    return time.Now().Unix() < int64(exp)
}

Testing

Mock Auth Server

// testing/mock_auth.go
package testing

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "time"
    "github.com/golang-jwt/jwt/v4"
)

type MockAuthServer struct {
    server *httptest.Server
}

func NewMockAuthServer() *MockAuthServer {
    mux := http.NewServeMux()
    server := httptest.NewServer(mux)
    
    m := &MockAuthServer{server: server}
    
    // Setup routes
    mux.HandleFunc("/realms/test/protocol/openid-connect/token", m.handleToken)
    mux.HandleFunc("/realms/test/protocol/openid-connect/certs", m.handleCerts)
    mux.HandleFunc("/realms/test/.well-known/openid-configuration", m.handleDiscovery)
    
    return m
}

func (m *MockAuthServer) handleToken(w http.ResponseWriter, r *http.Request) {
    // Generate mock token
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": "test-user",
        "exp": time.Now().Add(time.Hour).Unix(),
        "realm_access": map[string]interface{}{
            "roles": []string{"user", "admin"},
        },
    })
    
    tokenString, _ := token.SignedString([]byte("test-secret"))
    
    response := map[string]interface{}{
        "access_token": tokenString,
        "token_type":   "Bearer",
        "expires_in":   3600,
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func (m *MockAuthServer) URL() string {
    return m.server.URL
}

func (m *MockAuthServer) Close() {
    m.server.Close()
}

Integration Tests

// handlers/auth_test.go
package handlers

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestAuthMiddleware(t *testing.T) {
    // Setup mock auth server
    mockAuth := testing.NewMockAuthServer()
    defer mockAuth.Close()
    
    // Create test handler
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, ok := GetUser(r.Context())
        if !ok {
            t.Error("User not found in context")
            return
        }
        
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]string{
            "message": "Hello " + user.Username,
        })
    })
    
    // Wrap with auth middleware
    authHandler := RequireAuth(testConfig)(handler)
    
    // Test without token
    req := httptest.NewRequest("GET", "/protected", nil)
    rec := httptest.NewRecorder()
    authHandler.ServeHTTP(rec, req)
    
    if rec.Code != http.StatusTemporaryRedirect {
        t.Errorf("Expected redirect, got %d", rec.Code)
    }
    
    // Test with valid token
    req = httptest.NewRequest("GET", "/protected", nil)
    req.AddCookie(&http.Cookie{
        Name:  "session_id",
        Value: "valid-session",
    })
    rec = httptest.NewRecorder()
    authHandler.ServeHTTP(rec, req)
    
    if rec.Code != http.StatusOK {
        t.Errorf("Expected 200, got %d", rec.Code)
    }
}

Production Considerations

Circuit Breaker

// resilience/circuit_breaker.go
package resilience

import (
    "github.com/sony/gobreaker"
    "net/http"
    "time"
)

func NewAuthCircuitBreaker() *gobreaker.CircuitBreaker {
    settings := gobreaker.Settings{
        Name:        "auth-service",
        MaxRequests: 10,
        Interval:    60 * time.Second,
        Timeout:     30 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 3 && failureRatio >= 0.6
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            log.Printf("Circuit breaker %s: %s -> %s", name, from, to)
        },
    }
    
    return gobreaker.NewCircuitBreaker(settings)
}

// Usage with HTTP client
type ResilientAuthClient struct {
    client  *http.Client
    breaker *gobreaker.CircuitBreaker
}

func (c *ResilientAuthClient) Do(req *http.Request) (*http.Response, error) {
    resp, err := c.breaker.Execute(func() (interface{}, error) {
        return c.client.Do(req)
    })
    
    if err != nil {
        return nil, err
    }
    
    return resp.(*http.Response), nil
}

Metrics and Monitoring

// metrics/auth_metrics.go
package metrics

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    authRequests = promauto.NewCounterVec(prometheus.CounterOpts{
        Name: "auth_requests_total",
        Help: "Total number of authentication requests",
    }, []string{"status"})
    
    authDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
        Name: "auth_request_duration_seconds",
        Help: "Duration of authentication requests",
    }, []string{"operation"})
    
    tokenRefreshes = promauto.NewCounter(prometheus.CounterOpts{
        Name: "token_refreshes_total",
        Help: "Total number of token refreshes",
    })
)

// Middleware to track auth metrics
func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        timer := prometheus.NewTimer(authDuration.WithLabelValues("auth_check"))
        defer timer.ObserveDuration()
        
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(wrapped, r)
        
        authRequests.WithLabelValues(http.StatusText(wrapped.statusCode)).Inc()
    })
}

Health Checks

// health/auth_health.go
package health

import (
    "context"
    "net/http"
    "time"
)

type AuthHealthChecker struct {
    authURL    string
    httpClient *http.Client
}

func (h *AuthHealthChecker) Check(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    req, err := http.NewRequestWithContext(ctx, "GET", h.authURL+"/.well-known/openid-configuration", nil)
    if err != nil {
        return err
    }
    
    resp, err := h.httpClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("auth service unhealthy: status %d", resp.StatusCode)
    }
    
    return nil
}

Troubleshooting

Common Issues

  1. Token Verification Failures

    • Verify JWKS URL is accessible
    • Check clock synchronization
    • Ensure correct signing algorithm
  2. Session Management

    • Use secure session stores (Redis, etc.)
    • Implement proper session timeout
    • Handle concurrent session access
  3. Performance Issues

    • Cache JWKS keys
    • Implement token caching
    • Use connection pooling

Debug Logging

// Enable debug logging
func init() {
    if os.Getenv("AUTH_DEBUG") == "true" {
        log.SetLevel(log.DebugLevel)
        log.SetFormatter(&log.JSONFormatter{})
    }
}

Next Steps