forked from baron/baron-sso
IdP 연동 기능 devfront 이전 및 클라이언트 종속으로 개편
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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>}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user