forked from baron/baron-sso
클라이언트 비밀키 재발급(Rotate) API 구현
This commit is contained in:
@@ -613,6 +613,7 @@ func main() {
|
|||||||
dev.Post("/clients", devHandler.CreateClient)
|
dev.Post("/clients", devHandler.CreateClient)
|
||||||
dev.Get("/clients/:id", devHandler.GetClient)
|
dev.Get("/clients/:id", devHandler.GetClient)
|
||||||
dev.Put("/clients/:id", devHandler.UpdateClient)
|
dev.Put("/clients/:id", devHandler.UpdateClient)
|
||||||
|
dev.Post("/clients/:id/secret/rotate", devHandler.RotateClientSecret)
|
||||||
dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
|
dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
|
||||||
dev.Delete("/clients/:id", devHandler.DeleteClient)
|
dev.Delete("/clients/:id", devHandler.DeleteClient)
|
||||||
dev.Get("/consents", devHandler.ListConsents)
|
dev.Get("/consents", devHandler.ListConsents)
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import (
|
|||||||
"baron-sso-backend/internal/repository"
|
"baron-sso-backend/internal/repository"
|
||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -508,6 +511,71 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
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"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
||||||
status := "active"
|
status := "active"
|
||||||
if client.Metadata != nil {
|
if client.Metadata != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user