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.

143 lines
4.0 KiB
Go

// Package zitadel provides Zitadel authentication client for the KnowFoolery application.
package zitadel
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// Config holds the configuration for the Zitadel client.
type Config struct {
BaseURL string
ProjectID string
AdminToken string
Timeout time.Duration
}
// DefaultConfig returns a default configuration.
func DefaultConfig() Config {
return Config{
Timeout: 10 * time.Second,
}
}
// Client provides access to Zitadel authentication services.
type Client struct {
config Config
httpClient *http.Client
jwksCache *JWKSCache
}
// JWKSCache caches the JSON Web Key Set for token validation.
type JWKSCache struct {
//mu sync.RWMutex
keys map[string]interface{}
//expiry time.Time
duration time.Duration
}
// NewJWKSCache creates a new JWKS cache.
func NewJWKSCache(cacheDuration time.Duration) *JWKSCache {
return &JWKSCache{
keys: make(map[string]interface{}),
duration: cacheDuration,
}
}
// NewClient creates a new Zitadel client.
func NewClient(config Config) *Client {
return &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
jwksCache: NewJWKSCache(5 * time.Minute),
}
}
// AuthClaims represents the claims extracted from a validated JWT.
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"`
}
// TokenResponse represents a token response from Zitadel.
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// UserInfo represents user information from Zitadel.
type UserInfo struct {
ID string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Verified bool `json:"email_verified"`
Roles []string `json:"roles"`
}
// ValidateToken validates a JWT token and returns the claims.
// This is a placeholder implementation that should be replaced with actual JWT validation.
func (c *Client) ValidateToken(ctx context.Context, token string) (*AuthClaims, error) {
// TODO: Implement actual JWT validation with JWKS
// This is a placeholder that should:
// 1. Fetch JWKS from Zitadel
// 2. Parse and validate the JWT
// 3. Verify signature, expiration, issuer, audience
// 4. Return the claims
return nil, fmt.Errorf("not implemented: token validation requires JWKS integration")
}
// RefreshToken refreshes an access token using a refresh token.
func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) {
// TODO: Implement token refresh
return nil, fmt.Errorf("not implemented: token refresh")
}
// GetUserInfo retrieves user information using an access token.
func (c *Client) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error) {
url := fmt.Sprintf("%s/oidc/v1/userinfo", c.config.BaseURL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get user info: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get user info: status %d", resp.StatusCode)
}
var userInfo UserInfo
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return nil, fmt.Errorf("failed to decode user info: %w", err)
}
return &userInfo, nil
}
// RevokeToken revokes a token.
func (c *Client) RevokeToken(ctx context.Context, token string) error {
// TODO: Implement token revocation
return fmt.Errorf("not implemented: token revocation")
}