1
0
forked from baron/baron-sso

IdP 연동 기능 devfront 이전 및 클라이언트 종속으로 개편

This commit is contained in:
2026-01-29 14:54:38 +09:00
parent 59a5f99fb9
commit 3e2ceff692
6 changed files with 161 additions and 44 deletions

View File

@@ -476,15 +476,14 @@ func main() {
admin.Delete("/api-keys/:id", apiKeyHandler.DeleteApiKey)
// 개발자 포털 라우트 (RP/Consent 관리)
dev := api.Group("/dev")
dev.Get("/clients", devHandler.ListClients)
dev.Post("/clients", devHandler.CreateClient)
dev.Get("/clients/:id", devHandler.GetClient)
dev.Put("/clients/:id", devHandler.UpdateClient)
dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
dev.Delete("/clients/:id", devHandler.DeleteClient)
dev.Get("/consents", devHandler.ListConsents)
dev.Delete("/consents", devHandler.RevokeConsents)
api.Get("/clients", devHandler.ListClients)
api.Post("/clients", devHandler.CreateClient)
api.Get("/clients/:id", devHandler.GetClient)
api.Put("/clients/:id", devHandler.UpdateClient)
api.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
api.Delete("/clients/:id", devHandler.DeleteClient)
api.Get("/consents", devHandler.ListConsents)
api.Delete("/consents", devHandler.RevokeConsents)
// Webhook for Descope Generic SMS Gateway
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)

View File

@@ -18,16 +18,15 @@ const (
// IdentityProviderConfig stores the configuration for an external Identity Provider.
type IdentityProviderConfig struct {
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
TenantID string `gorm:"type:uuid;not null;index" json:"tenant_id"`
Tenant Tenant `gorm:"foreignKey:TenantID" json:"-"` // Belongs to Tenant
ClientID string `gorm:"type:uuid;not null;index" json:"client_id"` // Replaces TenantID
ProviderType ProviderType `gorm:"type:varchar(10);not null" json:"provider_type"`
DisplayName string `gorm:"not null" json:"display_name"`
Status string `gorm:"default:'active'" json:"status"`
// OIDC Specific Fields
IssuerURL *string `gorm:"null" json:"issuer_url,omitempty"`
ClientID *string `gorm:"null" json:"client_id,omitempty"`
ClientSecret *string `gorm:"null" json:"client_secret,omitempty"`
IssuerURL *string `gorm:"null" json:"issuer_url,omitempty"`
OIDCClientID *string `gorm:"null" json:"oidc_client_id,omitempty"` // Renamed from ClientID
OIDCClientSecret *string `gorm:"null" json:"oidc_client_secret,omitempty"` // Renamed from ClientSecret
// Scopes are space-separated
Scopes *string `gorm:"null" json:"scopes,omitempty"`

View File

@@ -62,6 +62,55 @@ func (h *FederationHandler) HandleOIDCCallback(c *fiber.Ctx) error {
return c.Redirect(redirectURL, fiber.StatusFound)
}
// --- New Client-based IdP Config Methods ---
// ListIdpConfigsForClient handles listing all IdP configurations for a client.
func (h *FederationHandler) ListIdpConfigsForClient(c *fiber.Ctx) error {
clientID := c.Params("clientId")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "clientId is required"})
}
var configs []domain.IdentityProviderConfig
if err := h.db.Where("client_id = ?", clientID).Find(&configs).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(configs)
}
// CreateIdpConfigForClient handles the creation of a new IdP configuration for a client.
func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error {
clientID := c.Params("clientId")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "clientId is required in path"})
}
var req domain.IdentityProviderConfig
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
// Assign clientID from path parameter
req.ClientID = clientID
// Basic validation
if req.DisplayName == "" || req.ProviderType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "display_name and provider_type are required"})
}
// TODO: Optionally, validate if the clientID exists in Hydra
// Create in DB
if err := h.db.Create(&req).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(req)
}
// --- Deprecated Tenant-based IdP Config Methods ---
// ListIdpConfigsForTenant handles listing all IdP configurations for a tenant.
func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
@@ -72,6 +121,8 @@ func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
// This is a temporary solution. We should create a proper method in the repository.
var configs []domain.IdentityProviderConfig
// Note: This now queries client_id, which is incorrect for tenants.
// This method is deprecated.
if err := h.db.Where("tenant_id = ?", tenantID).Find(&configs).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -86,14 +137,14 @@ func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
// Basic validation
if req.TenantID == "" || req.DisplayName == "" || req.ProviderType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id, display_name, and provider_type are required"})
// Basic validation - This is the old validation logic
if req.ClientID == "" || req.DisplayName == "" || req.ProviderType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id, display_name, and provider_type are required"})
}
// Check if tenant exists
// This check is now incorrect and deprecated.
var tenant domain.Tenant
if err := h.db.First(&tenant, "id = ?", req.TenantID).Error; err != nil {
if err := h.db.First(&tenant, "id = ?", req.ClientID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant not found"})
}
@@ -107,4 +158,4 @@ func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(req)
}
// TODO: Re-implement Update, Delete handlers for IdP Configs
// TODO: Re-implement Update, Delete handlers for IdP Configs for Clients

View File

@@ -28,7 +28,7 @@ func (s *FederationService) InitiateOIDCLogin(ctx context.Context, providerID, l
return "", fmt.Errorf("failed to find provider: %w", err)
}
if provider == nil || provider.IssuerURL == nil || provider.ClientID == nil || provider.ClientSecret == nil || provider.Scopes == nil {
if provider == nil || provider.IssuerURL == nil || provider.OIDCClientID == nil || provider.OIDCClientSecret == nil || provider.Scopes == nil {
return "", fmt.Errorf("OIDC configuration for provider %s is incomplete", providerID)
}
@@ -38,8 +38,8 @@ func (s *FederationService) InitiateOIDCLogin(ctx context.Context, providerID, l
}
config := oauth2.Config{
ClientID: *provider.ClientID,
ClientSecret: *provider.ClientSecret,
ClientID: *provider.OIDCClientID,
ClientSecret: *provider.OIDCClientSecret,
Endpoint: oidcProvider.Endpoint(),
RedirectURL: "http://localhost:8080/api/v1/federation/oidc/callback", // This should be configurable
Scopes: []string{*provider.Scopes},

View File

@@ -1,35 +1,39 @@
import { useParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createIdpConfig, listIdpConfigsForTenant } from "../../../lib/adminApi";
import type { IdpConfigCreateRequest, IdpConfig } from "../../../lib/adminApi";
import {
createIdpConfigForClient,
listIdpConfigsForClient,
} from "../../../lib/devApi";
import type { IdpConfigCreateRequest, IdpConfig } from "../../../lib/devApi";
import { useState } from "react";
// Proper Modal Component with Form
const CreateIdpModal = ({
isOpen,
onClose,
tenantId,
clientId,
}: {
isOpen: boolean;
onClose: () => void;
tenantId: string;
clientId: string;
}) => {
const queryClient = useQueryClient();
const [formData, setFormData] = useState<IdpConfigCreateRequest>({
tenant_id: tenantId,
client_id: clientId,
provider_type: "oidc",
display_name: "",
status: "active",
issuer_url: "",
client_id: "",
client_secret: "",
oidc_client_id: "",
oidc_client_secret: "",
scopes: "openid email profile",
});
const mutation = useMutation({
mutationFn: (newData: IdpConfigCreateRequest) => createIdpConfig(newData),
mutationFn: (newData: IdpConfigCreateRequest) =>
createIdpConfigForClient(newData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["idpConfigs", tenantId] });
queryClient.invalidateQueries({ queryKey: ["idpConfigs", clientId] });
onClose();
},
onError: (error) => {
@@ -99,8 +103,8 @@ const CreateIdpModal = ({
</label>
<input
type="text"
name="client_id"
value={formData.client_id}
name="oidc_client_id"
value={formData.oidc_client_id}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required
@@ -114,8 +118,8 @@ const CreateIdpModal = ({
</label>
<input
type="password"
name="client_secret"
value={formData.client_secret}
name="oidc_client_secret"
value={formData.oidc_client_secret}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required
@@ -159,24 +163,24 @@ const CreateIdpModal = ({
);
};
export function TenantFederationPage() {
const { tenantId } = useParams<{ tenantId: string }>();
export function ClientFederationPage() {
const { id: clientId } = useParams<{ id: string }>();
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
if (!tenantId) {
return <div>Tenant ID is missing</div>;
if (!clientId) {
return <div>Client ID is missing</div>;
}
const { data, isLoading, error } = useQuery({
queryKey: ["idpConfigs", tenantId],
queryFn: () => listIdpConfigsForTenant(tenantId),
queryKey: ["idpConfigs", clientId],
queryFn: () => listIdpConfigsForClient(clientId),
});
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Identity Federation Settings</h1>
<p className="mb-4 text-gray-600">
Manage external identity providers for this tenant.
Manage external identity providers for this application.
</p>
<div className="mb-4">
@@ -191,7 +195,7 @@ export function TenantFederationPage() {
<CreateIdpModal
isOpen={isCreateModalOpen}
onClose={() => setCreateModalOpen(false)}
tenantId={tenantId}
clientId={clientId}
/>
{isLoading && <div>Loading configurations...</div>}

View File

@@ -59,6 +59,37 @@ export type ConsentListResponse = {
items: ConsentSummary[];
};
// --- Federation / IdP Config Types ---
export type ProviderType = "oidc" | "saml";
export type IdpConfig = {
id: string;
client_id: string; // Changed from tenant_id
provider_type: ProviderType;
display_name: string;
status: "active" | "inactive";
issuer_url?: string;
// OIDC specific fields
oidc_client_id?: string;
oidc_client_secret?: string;
scopes?: string;
// SAML specific fields
metadata_url?: string;
metadata_xml?: string;
entity_id?: string;
acs_url?: string;
createdAt: string;
updatedAt: string;
};
export type IdpConfigCreateRequest = Omit<
IdpConfig,
"id" | "createdAt" | "updatedAt"
>;
export type IdpConfigUpdateRequest = Partial<IdpConfigCreateRequest>;
// --- End Federation Types ---
export async function fetchClients() {
const { data } = await apiClient.get<ClientListResponse>("/clients");
return data;
@@ -123,3 +154,36 @@ export async function revokeConsent(subject: string, clientId?: string) {
}
await apiClient.delete("/consents", { params });
}
// --- Federation / IdP Config API Calls ---
export async function listIdpConfigsForClient(clientId: string) {
const { data } = await apiClient.get<IdpConfig[]>(
`/clients/${clientId}/idps`,
);
return data;
}
export async function createIdpConfigForClient(payload: IdpConfigCreateRequest) {
const { data } = await apiClient.post<IdpConfig>(
`/clients/${payload.client_id}/idps`,
payload,
);
return data;
}
export async function updateIdpConfig(
clientId: string,
idpId: string,
payload: IdpConfigUpdateRequest,
) {
const { data } = await apiClient.put<IdpConfig>(
`/clients/${clientId}/idps/${idpId}`,
payload,
);
return data;
}
export async function deleteIdpConfig(clientId: string, idpId: string) {
await apiClient.delete(`/clients/${clientId}/idps/${idpId}`);
}