You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
28 KiB
28 KiB
Know Foolery - Zitadel Integration Guidelines
Zitadel Configuration
Project and Application Setup
# Zitadel Project Configuration
project:
name: "Know Foolery Quiz Game"
description: "Cross-platform quiz game authentication"
applications:
web_app:
name: "Know Foolery Web"
type: "WEB"
auth_method: "PKCE"
redirect_uris:
- "http://localhost:3000/auth/callback" # Development
- "https://app.knowfoolery.com/auth/callback" # Production
post_logout_redirect_uris:
- "http://localhost:3000/"
- "https://app.knowfoolery.com/"
mobile_app:
name: "Know Foolery Mobile"
type: "NATIVE"
auth_method: "PKCE"
redirect_uris:
- "knowfoolery://auth/callback"
admin_app:
name: "Know Foolery Admin"
type: "WEB"
auth_method: "PKCE"
redirect_uris:
- "http://localhost:3001/admin/auth/callback"
- "https://admin.knowfoolery.com/auth/callback"
roles:
- name: "player"
display_name: "Quiz Player"
description: "Can play quiz games and view leaderboards"
- name: "admin"
display_name: "Game Administrator"
description: "Can manage questions, users, and system settings"
policies:
password_policy:
min_length: 8
has_uppercase: true
has_lowercase: true
has_number: true
has_symbol: false
lockout_policy:
max_password_attempts: 5
show_lockout_failure: true
mfa_policy:
force_mfa: true # Required for admin role
force_mfa_local_only: false
Zitadel Repository Pattern Implementation
// ZitadelRepository interface for external integration
package auth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type ZitadelRepository interface {
ValidateToken(ctx context.Context, token string) (*AuthClaims, error)
RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error)
GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error)
RevokeToken(ctx context.Context, token string) error
CreateUser(ctx context.Context, user *CreateUserRequest) (*UserResponse, error)
GetUserByID(ctx context.Context, userID string) (*UserInfo, error)
UpdateUserRoles(ctx context.Context, userID string, roles []string) error
}
type zitadelRepository struct {
client *http.Client
baseURL string
projectID string
adminToken string
publicKeys map[string]*PublicKey
publicKeysExpiry time.Time
circuitBreaker *CircuitBreaker
}
type AuthClaims struct {
Subject string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Roles []string `json:"urn:zitadel:iam:org:project:roles"`
Audience []string `json:"aud"`
Issuer string `json:"iss"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
MFAVerified bool `json:"amr"`
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
type UserInfo struct {
ID string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Verified bool `json:"email_verified"`
Roles []string
}
func NewZitadelRepository(config ZitadelConfig) ZitadelRepository {
return &zitadelRepository{
client: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
},
},
baseURL: config.BaseURL,
projectID: config.ProjectID,
adminToken: config.AdminToken,
publicKeys: make(map[string]*PublicKey),
circuitBreaker: NewCircuitBreaker(),
}
}
func (r *zitadelRepository) ValidateToken(ctx context.Context, token string) (*AuthClaims, error) {
// Circuit breaker pattern for resilience
result, err := r.circuitBreaker.Execute(func() (interface{}, error) {
return r.validateTokenInternal(ctx, token)
})
if err == ErrCircuitOpen {
// Fallback: validate token locally if Zitadel is unavailable
return r.validateTokenLocally(token)
}
if err != nil {
return nil, fmt.Errorf("token validation failed: %w", err)
}
return result.(*AuthClaims), nil
}
func (r *zitadelRepository) validateTokenInternal(ctx context.Context, token string) (*AuthClaims, error) {
// Parse JWT token
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
// Verify signing algorithm
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Get public key from Zitadel JWKS endpoint
keyID := token.Header["kid"].(string)
return r.getPublicKey(ctx, keyID)
})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
if !parsedToken.Valid {
return nil, fmt.Errorf("invalid token")
}
// Extract claims
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
return r.extractAuthClaims(claims)
}
func (r *zitadelRepository) getPublicKey(ctx context.Context, keyID string) (*rsa.PublicKey, error) {
// Check cache first
if key, exists := r.publicKeys[keyID]; exists && time.Now().Before(r.publicKeysExpiry) {
return key.RSAPublicKey, nil
}
// Fetch from Zitadel JWKS endpoint
jwksURL := fmt.Sprintf("%s/.well-known/openid_configuration", r.baseURL)
req, err := http.NewRequestWithContext(ctx, "GET", jwksURL, nil)
if err != nil {
return nil, err
}
resp, err := r.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var jwks JWKS
if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
return nil, err
}
// Update cache
r.updatePublicKeysCache(jwks)
key, exists := r.publicKeys[keyID]
if !exists {
return nil, fmt.Errorf("key ID %s not found", keyID)
}
return key.RSAPublicKey, nil
}
func (r *zitadelRepository) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) {
data := url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {refreshToken},
"client_id": {r.projectID},
}
req, err := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/oauth/v2/token", r.baseURL),
strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := r.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token refresh failed with status: %d", resp.StatusCode)
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, err
}
return &tokenResp, nil
}
func (r *zitadelRepository) CreateUser(ctx context.Context, user *CreateUserRequest) (*UserResponse, error) {
reqBody := map[string]interface{}{
"userName": user.Email,
"profile": map[string]interface{}{
"firstName": user.FirstName,
"lastName": user.LastName,
"displayName": user.DisplayName,
},
"email": map[string]interface{}{
"email": user.Email,
"isEmailVerified": false,
},
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/management/v1/users/human", r.baseURL),
bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.adminToken))
req.Header.Set("Content-Type", "application/json")
resp, err := r.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var userResp UserResponse
if err := json.NewDecoder(resp.Body).Decode(&userResp); err != nil {
return nil, err
}
return &userResp, nil
}
// Circuit breaker implementation for resilience
type CircuitBreaker struct {
state CircuitState
failureCount int
threshold int
timeout time.Duration
lastFailTime time.Time
mutex sync.RWMutex
}
type CircuitState int
const (
Closed CircuitState = iota
Open
HalfOpen
)
var ErrCircuitOpen = errors.New("circuit breaker is open")
func NewCircuitBreaker() *CircuitBreaker {
return &CircuitBreaker{
state: Closed,
threshold: 5,
timeout: 30 * time.Second,
}
}
func (cb *CircuitBreaker) Execute(fn func() (interface{}, error)) (interface{}, error) {
cb.mutex.RLock()
state := cb.state
cb.mutex.RUnlock()
switch state {
case Open:
if time.Since(cb.lastFailTime) > cb.timeout {
cb.setState(HalfOpen)
return cb.tryExecute(fn)
}
return nil, ErrCircuitOpen
case HalfOpen:
return cb.tryExecute(fn)
default: // Closed
return cb.tryExecute(fn)
}
}
func (cb *CircuitBreaker) tryExecute(fn func() (interface{}, error)) (interface{}, error) {
result, err := fn()
cb.mutex.Lock()
defer cb.mutex.Unlock()
if err != nil {
cb.failureCount++
cb.lastFailTime = time.Now()
if cb.failureCount >= cb.threshold {
cb.state = Open
}
return nil, err
}
// Success - reset circuit breaker
cb.failureCount = 0
cb.state = Closed
return result, nil
}
func (cb *CircuitBreaker) setState(state CircuitState) {
cb.mutex.Lock()
defer cb.mutex.Unlock()
cb.state = state
}
Authentication Flows
Zitadel Authentication Flow
// Secure authentication implementation
package auth
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/argon2"
)
type SecureAuthService struct {
zitadelRepo ZitadelRepository
keyStore *KeyStore
sessionManager *SessionManager
auditLogger *AuditLogger
rateLimiter *RateLimiter
}
type AuthClaims struct {
jwt.RegisteredClaims
Email string `json:"email"`
Name string `json:"name"`
Roles []string `json:"urn:zitadel:iam:org:project:roles"`
MFAVerified bool `json:"amr"`
SessionID string `json:"sid"`
DeviceID string `json:"device_id,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
}
// Token validation with comprehensive security checks
func (s *SecureAuthService) ValidateToken(ctx context.Context, tokenString string, clientIP string) (*AuthClaims, error) {
// Rate limiting check
if !s.rateLimiter.Allow(fmt.Sprintf("token_validation:%s", clientIP)) {
s.auditLogger.LogSecurityEvent("rate_limit_exceeded", "", clientIP, "warning", map[string]interface{}{
"operation": "token_validation",
})
return nil, fmt.Errorf("rate limit exceeded")
}
// Parse and validate JWT structure
token, err := jwt.ParseWithClaims(tokenString, &AuthClaims{}, func(token *jwt.Token) (interface{}, error) {
// Verify signing algorithm
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// Get key ID from header
keyID, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("missing key ID in token header")
}
// Retrieve public key
return s.keyStore.GetPublicKey(keyID)
})
if err != nil {
s.auditLogger.LogSecurityEvent("invalid_token", "", clientIP, "warning", map[string]interface{}{
"error": err.Error(),
})
return nil, fmt.Errorf("token validation failed: %w", err)
}
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(*AuthClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
// Additional security validations
if err := s.validateTokenClaims(claims, clientIP); err != nil {
s.auditLogger.LogSecurityEvent("token_validation_failed", claims.Subject, clientIP, "warning", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
// Check if session is still valid
if !s.sessionManager.IsValidSession(claims.SessionID, claims.Subject) {
s.auditLogger.LogSecurityEvent("invalid_session", claims.Subject, clientIP, "warning", map[string]interface{}{
"session_id": claims.SessionID,
})
return nil, fmt.Errorf("session no longer valid")
}
// Log successful validation
s.auditLogger.LogSecurityEvent("token_validated", claims.Subject, clientIP, "info", map[string]interface{}{
"session_id": claims.SessionID,
"roles": claims.Roles,
})
return claims, nil
}
func (s *SecureAuthService) validateTokenClaims(claims *AuthClaims, clientIP string) error {
now := time.Now()
// Check expiration
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(now) {
return fmt.Errorf("token expired")
}
// Check not before
if claims.NotBefore != nil && claims.NotBefore.Time.After(now) {
return fmt.Errorf("token not yet valid")
}
// Check issued at (prevent future tokens)
if claims.IssuedAt != nil && claims.IssuedAt.Time.After(now.Add(5*time.Minute)) {
return fmt.Errorf("token issued in the future")
}
// Validate audience
expectedAudience := "knowfoolery-quiz-game"
if !contains(claims.Audience, expectedAudience) {
return fmt.Errorf("invalid audience")
}
// Validate issuer
expectedIssuer := "https://auth.knowfoolery.com"
if claims.Issuer != expectedIssuer {
return fmt.Errorf("invalid issuer")
}
// IP address validation (if configured)
if claims.IPAddress != "" && claims.IPAddress != clientIP {
return fmt.Errorf("IP address mismatch")
}
return nil
}
// Multi-Factor Authentication enforcement
func (s *SecureAuthService) RequireMFA(userID string, roles []string) bool {
// Admin users always require MFA
for _, role := range roles {
if role == "admin" {
return true
}
}
// Check if user has high-value permissions
return s.hasHighValuePermissions(roles)
}
func (s *SecureAuthService) hasHighValuePermissions(roles []string) bool {
highValueRoles := []string{"moderator", "question_manager", "user_manager"}
for _, userRole := range roles {
for _, hvRole := range highValueRoles {
if userRole == hvRole {
return true
}
}
}
return false
}
Player Authentication Flow (Web/Mobile)
// Frontend authentication service
export class AuthService {
private zitadelConfig: ZitadelConfig
constructor(config: ZitadelConfig) {
this.zitadelConfig = config
}
async loginPlayer(): Promise<AuthResult> {
// PKCE flow for security
const codeVerifier = this.generateCodeVerifier()
const codeChallenge = await this.generateCodeChallenge(codeVerifier)
// Store code verifier securely
await this.secureStorage.store('code_verifier', codeVerifier)
// Redirect to Zitadel authorization endpoint
const authURL = new URL(`${this.zitadelConfig.baseURL}/oauth/v2/authorize`)
authURL.searchParams.set('client_id', this.zitadelConfig.clientId)
authURL.searchParams.set('response_type', 'code')
authURL.searchParams.set('scope', 'openid profile email urn:zitadel:iam:org:project:roles')
authURL.searchParams.set('redirect_uri', this.zitadelConfig.redirectUri)
authURL.searchParams.set('code_challenge', codeChallenge)
authURL.searchParams.set('code_challenge_method', 'S256')
authURL.searchParams.set('state', this.generateState())
// Platform-specific redirect
if (this.isPlatform('web')) {
window.location.href = authURL.toString()
} else if (this.isPlatform('mobile')) {
await this.openAuthSession(authURL.toString())
}
}
async handleAuthCallback(code: string, state: string): Promise<TokenResult> {
// Verify state parameter
const storedState = await this.secureStorage.get('auth_state')
if (state !== storedState) {
throw new Error('Invalid state parameter')
}
// Exchange code for tokens
const codeVerifier = await this.secureStorage.get('code_verifier')
const tokenResponse = await fetch(`${this.zitadelConfig.baseURL}/oauth/v2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.zitadelConfig.clientId,
code,
redirect_uri: this.zitadelConfig.redirectUri,
code_verifier: codeVerifier,
}),
})
if (!tokenResponse.ok) {
throw new Error('Token exchange failed')
}
const tokens = await tokenResponse.json()
// Store tokens securely
await this.storeTokens(tokens)
// Decode and validate JWT claims
const userInfo = await this.getUserInfo(tokens.access_token)
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
userInfo,
}
}
async refreshAccessToken(): Promise<string> {
const refreshToken = await this.secureStorage.get('refresh_token')
if (!refreshToken) {
throw new Error('No refresh token available')
}
const response = await fetch(`${this.zitadelConfig.baseURL}/oauth/v2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.zitadelConfig.clientId,
}),
})
if (!response.ok) {
// Refresh token expired, need to re-authenticate
await this.logout()
throw new Error('Refresh token expired')
}
const tokens = await response.json()
await this.storeTokens(tokens)
return tokens.access_token
}
private async storeTokens(tokens: TokenResponse): Promise<void> {
// Platform-specific secure storage
if (this.isPlatform('web')) {
// Use secure HttpOnly cookies for web
await this.apiClient.post('/auth/store-tokens', { tokens })
} else if (this.isPlatform('mobile')) {
// Use Keychain/Keystore for mobile
await this.secureStorage.store('access_token', tokens.access_token)
await this.secureStorage.store('refresh_token', tokens.refresh_token)
}
}
}
Admin Authentication Flow with MFA
export class AdminAuthService extends AuthService {
async loginAdmin(): Promise<AuthResult> {
// Standard OAuth flow first
const authResult = await super.loginPlayer()
// Check if user has admin role
if (!authResult.userInfo.roles.includes('admin')) {
throw new Error('Insufficient permissions')
}
// Verify MFA is completed
if (!authResult.userInfo.mfaVerified) {
return this.redirectToMFA()
}
return authResult
}
private async redirectToMFA(): Promise<AuthResult> {
// Redirect to Zitadel MFA setup/verification
const mfaURL = new URL(`${this.zitadelConfig.baseURL}/ui/login/mfa/init`)
if (this.isPlatform('web')) {
window.location.href = mfaURL.toString()
}
// Handle MFA callback
return this.handleMFACallback()
}
private async handleMFACallback(): Promise<AuthResult> {
// After MFA completion, refresh token to get updated claims
const newAccessToken = await this.refreshAccessToken()
const userInfo = await this.getUserInfo(newAccessToken)
if (!userInfo.mfaVerified) {
throw new Error('MFA verification failed')
}
return {
accessToken: newAccessToken,
userInfo,
}
}
}
Backend JWT Validation
JWT Middleware Implementation
// JWT validation middleware for API Gateway
package middleware
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
type JWTMiddleware struct {
zitadelRepo auth.ZitadelRepository
config JWTConfig
}
type JWTConfig struct {
Issuer string
Audience string
RequiredClaims []string
AdminEndpoints []string
RateLimitByUserID bool
}
func NewJWTMiddleware(zitadelRepo auth.ZitadelRepository, config JWTConfig) *JWTMiddleware {
return &JWTMiddleware{
zitadelRepo: zitadelRepo,
config: config,
}
}
func (m *JWTMiddleware) ValidateToken() fiber.Handler {
return func(c *fiber.Ctx) error {
// Extract token from Authorization header
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(401).JSON(fiber.Map{"error": "Authorization header required"})
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
return c.Status(401).JSON(fiber.Map{"error": "Invalid authorization header format"})
}
// Validate token with Zitadel
ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
defer cancel()
claims, err := m.zitadelRepo.ValidateToken(ctx, tokenString)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Invalid token", "details": err.Error()})
}
// Validate claims
if err := m.validateClaims(claims); err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Invalid token claims", "details": err.Error()})
}
// Set user context
c.Locals("user_id", claims.Subject)
c.Locals("user_email", claims.Email)
c.Locals("user_name", claims.Name)
c.Locals("user_roles", claims.Roles)
c.Locals("mfa_verified", claims.MFAVerified)
// Check admin access for admin endpoints
if m.isAdminEndpoint(c.Path()) {
if !m.hasAdminRole(claims.Roles) {
return c.Status(403).JSON(fiber.Map{"error": "Admin access required"})
}
if !claims.MFAVerified {
return c.Status(403).JSON(fiber.Map{"error": "MFA verification required for admin access"})
}
}
return c.Next()
}
}
func (m *JWTMiddleware) validateClaims(claims *auth.AuthClaims) error {
// Validate issuer
if claims.Issuer != m.config.Issuer {
return fmt.Errorf("invalid issuer: expected %s, got %s", m.config.Issuer, claims.Issuer)
}
// Validate audience
validAudience := false
for _, aud := range claims.Audience {
if aud == m.config.Audience {
validAudience = true
break
}
}
if !validAudience {
return fmt.Errorf("invalid audience")
}
// Validate expiration
if time.Unix(claims.ExpiresAt, 0).Before(time.Now()) {
return fmt.Errorf("token expired")
}
// Validate required claims
for _, requiredClaim := range m.config.RequiredClaims {
switch requiredClaim {
case "email":
if claims.Email == "" {
return fmt.Errorf("email claim required")
}
case "name":
if claims.Name == "" {
return fmt.Errorf("name claim required")
}
case "roles":
if len(claims.Roles) == 0 {
return fmt.Errorf("roles claim required")
}
}
}
return nil
}
func (m *JWTMiddleware) isAdminEndpoint(path string) bool {
for _, adminPath := range m.config.AdminEndpoints {
if strings.HasPrefix(path, adminPath) {
return true
}
}
return false
}
func (m *JWTMiddleware) hasAdminRole(roles []string) bool {
for _, role := range roles {
if role == "admin" {
return true
}
}
return false
}
// Token refresh middleware
func (m *JWTMiddleware) RefreshTokenIfNeeded() fiber.Handler {
return func(c *fiber.Ctx) error {
userID := c.Locals("user_id")
if userID == nil {
return c.Next()
}
// Check if token is close to expiration (within 5 minutes)
tokenString := strings.TrimPrefix(c.Get("Authorization"), "Bearer ")
token, err := jwt.Parse(tokenString, nil)
if err != nil {
// Token parsing failed, but that's handled by ValidateToken middleware
return c.Next()
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if exp, ok := claims["exp"].(float64); ok {
expirationTime := time.Unix(int64(exp), 0)
if time.Until(expirationTime) < 5*time.Minute {
// Signal frontend to refresh token
c.Set("X-Token-Refresh-Required", "true")
}
}
}
return c.Next()
}
}
Monitoring and Metrics
Zitadel Integration Metrics
// Metrics for Zitadel integration monitoring
package metrics
import (
"context"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
zitadelRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "zitadel_requests_total",
Help: "Total number of requests to Zitadel",
},
[]string{"operation", "status"},
)
zitadelRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "zitadel_request_duration_seconds",
Help: "Duration of Zitadel requests",
Buckets: prometheus.DefBuckets,
},
[]string{"operation"},
)
authenticationAttempts = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "authentication_attempts_total",
Help: "Total number of authentication attempts",
},
[]string{"method", "result"},
)
circuitBreakerState = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "circuit_breaker_state",
Help: "Circuit breaker state (0=closed, 1=open, 2=half-open)",
},
[]string{"service"},
)
)
// Instrumented Zitadel repository
type InstrumentedZitadelRepository struct {
repo auth.ZitadelRepository
}
func NewInstrumentedZitadelRepository(repo auth.ZitadelRepository) auth.ZitadelRepository {
return &InstrumentedZitadelRepository{repo: repo}
}
func (r *InstrumentedZitadelRepository) ValidateToken(ctx context.Context, token string) (*auth.AuthClaims, error) {
start := time.Now()
claims, err := r.repo.ValidateToken(ctx, token)
duration := time.Since(start).Seconds()
status := "success"
if err != nil {
status = "error"
}
zitadelRequestsTotal.WithLabelValues("validate_token", status).Inc()
zitadelRequestDuration.WithLabelValues("validate_token").Observe(duration)
if err == nil {
authenticationAttempts.WithLabelValues("jwt", "success").Inc()
} else {
authenticationAttempts.WithLabelValues("jwt", "failure").Inc()
}
return claims, err
}
func (r *InstrumentedZitadelRepository) RefreshToken(ctx context.Context, refreshToken string) (*auth.TokenResponse, error) {
start := time.Now()
tokenResp, err := r.repo.RefreshToken(ctx, refreshToken)
duration := time.Since(start).Seconds()
status := "success"
if err != nil {
status = "error"
}
zitadelRequestsTotal.WithLabelValues("refresh_token", status).Inc()
zitadelRequestDuration.WithLabelValues("refresh_token").Observe(duration)
return tokenResp, err
}
This Zitadel integration architecture provides secure, scalable authentication with proper monitoring and resilience patterns for the Know Foolery quiz game.