1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/handler/dev_handler.go

808 lines
25 KiB
Go

package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type DevHandler struct {
Hydra *service.HydraAdminService
Redis domain.RedisRepository
SecretRepo domain.ClientSecretRepository
KratosAdmin *service.KratosAdminService
ConsentRepo repository.ClientConsentRepository
Keto service.KetoService
RPSvc service.RelyingPartyService
}
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService, keto service.KetoService) *DevHandler {
return &DevHandler{
Hydra: service.NewHydraAdminService(),
Redis: redis,
SecretRepo: secretRepo,
KratosAdmin: service.NewKratosAdminService(),
ConsentRepo: consentRepo,
Keto: keto,
RPSvc: rpSvc,
}
}
type clientSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
RedirectURIs []string `json:"redirectUris"`
Scopes []string `json:"scopes"`
ClientSecret string `json:"clientSecret,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type clientListResponse struct {
Items []clientSummary `json:"items"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
type clientDetailResponse struct {
Client clientSummary `json:"client"`
Endpoints clientEndpoints `json:"endpoints"`
}
type clientEndpoints struct {
Discovery string `json:"discovery"`
Issuer string `json:"issuer"`
Authorization string `json:"authorization"`
Token string `json:"token"`
UserInfo string `json:"userinfo"`
}
type consentSummary struct {
Subject string `json:"subject"`
UserName string `json:"userName,omitempty"`
ClientID string `json:"clientId"`
ClientName string `json:"clientName,omitempty"`
GrantedScopes []string `json:"grantedScopes"`
AuthenticatedAt string `json:"authenticatedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"`
}
type consentListResponse struct {
Items []consentSummary `json:"items"`
}
type clientUpsertRequest struct {
ID *string `json:"id"`
Name *string `json:"name"`
Type *string `json:"type"`
Status *string `json:"status"`
RedirectURIs *[]string `json:"redirectUris"`
Scopes *[]string `json:"scopes"`
GrantTypes *[]string `json:"grantTypes"`
ResponseTypes *[]string `json:"responseTypes"`
TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"`
Metadata *map[string]interface{} `json:"metadata"`
}
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
if !ok || profile == nil {
return false, nil
}
// Super Admin bypass
if profile.Role == domain.RoleSuperAdmin {
return true, nil
}
// Check with Keto: System:AppManager#member
allowed, err := h.Keto.CheckPermission(c.Context(), profile.ID, "System", "AppManager", "member")
if err != nil {
return false, err
}
return allowed, nil
}
func (h *DevHandler) ListClients(c *fiber.Ctx) error {
limit := c.QueryInt("limit", 50)
offset := c.QueryInt("offset", 0)
if limit <= 0 {
limit = 50
}
if offset < 0 {
offset = 0
}
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
slog.Error("Failed to check app manager permission", "error", err)
}
clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "clients not found"})
}
errMsg := err.Error()
if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "dial tcp") {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"error": "Hydra service is unavailable. Please check if Ory Hydra is running.",
})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errMsg})
}
items := make([]clientSummary, 0, len(clients))
for _, client := range clients {
summary := h.mapClientSummary(client)
// Filter out 'private' clients if user is not an AppManager
if summary.Type == "private" && !isAppManager {
continue
}
items = append(items, summary)
}
return c.JSON(clientListResponse{
Items: items,
Limit: limit,
Offset: offset,
})
}
func (h *DevHandler) GetClient(c *fiber.Ctx) error {
clientID := c.Params("id")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
}
client, err := h.Hydra.GetClient(c.Context(), clientID)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
summary := h.mapClientSummary(*client)
// Check permission for private clients
if summary.Type == "private" {
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
}
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
}
}
return c.JSON(clientDetailResponse{
Client: summary,
Endpoints: clientEndpoints{
Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
Issuer: h.Hydra.PublicURL,
Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
},
})
}
func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
clientID := c.Params("id")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
}
var req struct {
Status string `json:"status"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
status := strings.ToLower(strings.TrimSpace(req.Status))
if status != "active" && status != "inactive" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
}
// [Security] Check permission before patching
current, err := h.Hydra.GetClient(c.Context(), clientID)
if err == nil {
summary := h.mapClientSummary(*current)
if summary.Type == "private" {
isAppManager, _ := h.checkAppManagerPermission(c)
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
}
}
}
updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
summary := h.mapClientSummary(*updated)
return c.JSON(clientDetailResponse{
Client: summary,
Endpoints: clientEndpoints{
Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
Issuer: h.Hydra.PublicURL,
Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
},
})
}
func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
var req clientUpsertRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
clientID := strings.TrimSpace(valueOr(req.ID, ""))
if clientID == "" {
clientID = uuid.NewString()
}
name := strings.TrimSpace(valueOr(req.Name, ""))
if name == "" {
name = clientID
}
redirectURIs := derefSlice(req.RedirectURIs, nil)
if len(redirectURIs) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris is required"})
}
scopes := derefSlice(req.Scopes, defaultClientScopes())
grantTypes := derefSlice(req.GrantTypes, defaultGrantTypes())
responseTypes := derefSlice(req.ResponseTypes, defaultResponseTypes())
clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "private")))
if clientType != "pkce" && clientType != "private" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
}
// [Security] Check permission for private clients
if clientType == "private" {
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
}
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions to create private client"})
}
}
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
if status != "active" && status != "inactive" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
}
metadata := mergeMetadata(nil, req.Metadata)
if metadata == nil {
metadata = map[string]interface{}{}
}
metadata["status"] = status
metadata["created_at"] = time.Now().Format(time.RFC3339)
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
if tokenAuthMethod == "" {
if clientType == "pkce" {
tokenAuthMethod = "none"
} else {
tokenAuthMethod = "client_secret_basic"
}
}
clientReq := domain.HydraClient{
ClientID: clientID,
ClientName: name,
RedirectURIs: redirectURIs,
GrantTypes: grantTypes,
ResponseTypes: responseTypes,
Scope: strings.Join(scopes, " "),
TokenEndpointAuthMethod: tokenAuthMethod,
Metadata: metadata,
}
created, err := h.Hydra.CreateClient(c.Context(), clientReq)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// Store secret in metadata for later retrieval
if created.ClientSecret != "" {
// 1. Store in PostgreSQL (Source of Truth)
if h.SecretRepo != nil {
_ = h.SecretRepo.Upsert(c.Context(), created.ClientID, created.ClientSecret)
}
// 2. Also store in Redis (Cache)
if h.Redis != nil {
_ = h.Redis.Set("client_secret:"+created.ClientID, created.ClientSecret, 0)
}
}
summary := h.mapClientSummary(*created)
return c.Status(fiber.StatusCreated).JSON(clientDetailResponse{
Client: summary,
Endpoints: clientEndpoints{
Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
Issuer: h.Hydra.PublicURL,
Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
},
})
}
func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Params("id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
}
var req clientUpsertRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
current, err := h.Hydra.GetClient(c.Context(), clientID)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
clientType := ""
if req.Type != nil {
clientType = strings.ToLower(strings.TrimSpace(*req.Type))
if clientType != "pkce" && clientType != "private" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
}
}
// [Security] Check permission for private clients (both current and new type)
currentSummary := h.mapClientSummary(*current)
if currentSummary.Type == "private" || clientType == "private" {
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
}
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
}
}
status := ""
if req.Status != nil {
status = strings.ToLower(strings.TrimSpace(*req.Status))
if status != "active" && status != "inactive" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
}
}
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
if tokenAuthMethod == "" && clientType != "" {
if clientType == "pkce" {
tokenAuthMethod = "none"
} else {
tokenAuthMethod = "client_secret_basic"
}
}
if req.RedirectURIs != nil && len(*req.RedirectURIs) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris cannot be empty"})
}
metadata := mergeMetadata(current.Metadata, req.Metadata)
if status != "" {
if metadata == nil {
metadata = map[string]interface{}{}
}
metadata["status"] = status
}
updated := domain.HydraClient{
ClientID: current.ClientID,
ClientName: valueOr(req.Name, current.ClientName),
RedirectURIs: derefSlice(req.RedirectURIs, current.RedirectURIs),
GrantTypes: derefSlice(req.GrantTypes, current.GrantTypes),
ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes),
Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))),
TokenEndpointAuthMethod: resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod),
Metadata: metadata,
}
updatedClient, err := h.Hydra.UpdateClient(c.Context(), clientID, updated)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
summary := h.mapClientSummary(*updatedClient)
return c.JSON(clientDetailResponse{
Client: summary,
Endpoints: clientEndpoints{
Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
Issuer: h.Hydra.PublicURL,
Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
},
})
}
func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Params("id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
}
// [Security] Check permission for private clients
current, err := h.Hydra.GetClient(c.Context(), clientID)
if err == nil {
summary := h.mapClientSummary(*current)
if summary.Type == "private" {
isAppManager, _ := h.checkAppManagerPermission(c)
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
}
}
}
if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// 1. Clean up PostgreSQL
if h.SecretRepo != nil {
_ = h.SecretRepo.Delete(c.Context(), clientID)
}
// 2. Clean up Redis
if h.Redis != nil {
_ = h.Redis.Delete("client_secret:" + clientID)
}
return c.SendStatus(fiber.StatusNoContent)
}
func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Query("client_id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id is required"})
}
subject := strings.TrimSpace(c.Query("subject"))
limit := c.QueryInt("limit", 50)
offset := c.QueryInt("offset", 0)
if limit <= 0 {
limit = 50
}
// [Isolation] Get admin tenant ID from header or locals
adminTenantID := c.Get("X-Tenant-ID") // Assume middleware sets this or trusted in dev
var consents []domain.ClientConsentWithTenantInfo
var total int64
var err error
if subject != "" {
// Resolve subject if it's email/name (Legacy support)
if _, err := uuid.Parse(subject); err != nil {
resolved, _ := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), subject)
if resolved != "" {
subject = resolved
}
}
// Single user fetch from Hydra (to get latest status) or Local DB
// Issue says: "List All", so we prefer Local DB for consistency in listing
// But for a single user, we could still use Hydra.
// Let's use Local DB to support tenant filtering even for search.
// For simplicity, we just filter the list later if search is used.
}
if adminTenantID != "" {
consents, total, err = h.ConsentRepo.ListByTenant(c.Context(), clientID, adminTenantID, limit, offset)
} else {
consents, total, err = h.ConsentRepo.List(c.Context(), clientID, limit, offset)
}
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
items := make([]consentSummary, 0, len(consents))
for _, consent := range consents {
// Filter by subject if search is active
if subject != "" && consent.Subject != subject {
continue
}
userName := ""
identity, err := h.KratosAdmin.GetIdentity(c.Context(), consent.Subject)
if err == nil && identity != nil {
if name, ok := identity.Traits["name"].(string); ok {
userName = name
} else if email, ok := identity.Traits["email"].(string); ok {
userName = email
}
}
items = append(items, consentSummary{
Subject: consent.Subject,
UserName: userName,
ClientID: consent.ClientID,
GrantedScopes: consent.GrantedScopes,
AuthenticatedAt: consent.UpdatedAt.Format(time.RFC3339),
CreatedAt: consent.CreatedAt,
TenantID: consent.TenantID,
TenantName: consent.TenantName,
})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
})
}
func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
subject := strings.TrimSpace(c.Query("subject"))
if subject == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"})
}
clientID := strings.TrimSpace(c.Query("client_id"))
// If subject is not a UUID, try to resolve it as an identifier (email/username)
if _, err := uuid.Parse(subject); err != nil {
resolved, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), subject)
if err == nil && resolved != "" {
subject = resolved
}
}
// 1. Revoke in Hydra
if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// 2. Sync to Local DB (Delete)
if h.ConsentRepo != nil {
_ = h.ConsentRepo.Delete(c.Context(), subject, clientID)
}
return c.SendStatus(fiber.StatusNoContent)
}
func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Params("id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
}
// [Security] Check permission for private clients
current, err := h.Hydra.GetClient(c.Context(), clientID)
if err == nil {
summary := h.mapClientSummary(*current)
if summary.Type == "private" {
isAppManager, _ := h.checkAppManagerPermission(c)
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
}
}
}
// 1. Generate new secret
newSecret, err := generateRandomSecret(20)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate secret"})
}
// 2. Get current client to preserve other fields (already fetched above)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// 3. Update Hydra
current.ClientSecret = newSecret
updated, err := h.Hydra.UpdateClient(c.Context(), clientID, *current)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
// 4. Update Persistence (DB & Redis)
if h.SecretRepo != nil {
if err := h.SecretRepo.Upsert(c.Context(), clientID, newSecret); err != nil {
// Log error but don't fail the request as Hydra is already updated
fmt.Printf("failed to update secret in repo: %v\n", err)
}
}
if h.Redis != nil {
_ = h.Redis.Set("client_secret:"+clientID, newSecret, 0)
}
// Return the new secret
summary := h.mapClientSummary(*updated)
summary.ClientSecret = newSecret
return c.JSON(clientDetailResponse{
Client: summary,
Endpoints: clientEndpoints{
Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
Issuer: h.Hydra.PublicURL,
Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
},
})
}
func generateRandomSecret(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
// Use Base64 URL encoding (no padding) to look like Hydra's native secrets
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
status := "active"
var createdAt *time.Time
if client.Metadata != nil {
if value, ok := client.Metadata["status"].(string); ok && strings.ToLower(value) == "inactive" {
status = "inactive"
}
if value, ok := client.Metadata["created_at"].(string); ok {
if t, err := time.Parse(time.RFC3339, value); err == nil {
createdAt = &t
}
}
}
clientType := "private"
if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
clientType = "pkce"
}
name := strings.TrimSpace(client.ClientName)
if name == "" {
name = client.ClientID
}
scopes := strings.Fields(client.Scope)
clientSecret := client.ClientSecret
// 1. Check Metadata (Legacy/Fallback)
if clientSecret == "" && client.Metadata != nil {
if val, ok := client.Metadata["client_secret"].(string); ok {
clientSecret = val
}
}
// 2. Check Redis (Cache)
if clientSecret == "" && h.Redis != nil {
if val, err := h.Redis.Get("client_secret:" + client.ClientID); err == nil && val != "" {
clientSecret = val
}
}
// 3. Check PostgreSQL (Source of Truth) & Cache Warming
if clientSecret == "" && h.SecretRepo != nil {
if val, err := h.SecretRepo.GetByID(context.Background(), client.ClientID); err == nil && val != "" {
clientSecret = val
// Warm up cache
if h.Redis != nil {
_ = h.Redis.Set("client_secret:"+client.ClientID, clientSecret, 0)
}
}
}
return clientSummary{
ID: client.ClientID,
Name: name,
Type: clientType,
Status: status,
CreatedAt: createdAt,
RedirectURIs: client.RedirectURIs,
Scopes: scopes,
ClientSecret: clientSecret,
Metadata: client.Metadata,
}
}
func defaultClientScopes() []string {
return []string{"openid", "profile", "email"}
}
func defaultGrantTypes() []string {
return []string{"authorization_code", "refresh_token"}
}
func defaultResponseTypes() []string {
return []string{"code"}
}
func buildScope(scopes []string) string {
return strings.Join(scopes, " ")
}
func valueOr(ptr *string, fallback string) string {
if ptr == nil {
return fallback
}
return *ptr
}
func valueOrSlice(ptr *[]string, fallback []string) []string {
if ptr == nil {
return fallback
}
return *ptr
}
func derefSlice(ptr *[]string, fallback []string) []string {
if ptr == nil {
return fallback
}
return *ptr
}
func mergeMetadata(current map[string]interface{}, incoming *map[string]interface{}) map[string]interface{} {
if incoming == nil {
return current
}
merged := map[string]interface{}{}
for k, v := range current {
merged[k] = v
}
for k, v := range *incoming {
merged[k] = v
}
return merged
}
func resolveTokenAuthMethod(requested, fallback string) string {
if strings.TrimSpace(requested) == "" {
return fallback
}
return requested
}