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)
|
admin.Delete("/api-keys/:id", apiKeyHandler.DeleteApiKey)
|
||||||
|
|
||||||
// 개발자 포털 라우트 (RP/Consent 관리)
|
// 개발자 포털 라우트 (RP/Consent 관리)
|
||||||
dev := api.Group("/dev")
|
api.Get("/clients", devHandler.ListClients)
|
||||||
dev.Get("/clients", devHandler.ListClients)
|
api.Post("/clients", devHandler.CreateClient)
|
||||||
dev.Post("/clients", devHandler.CreateClient)
|
api.Get("/clients/:id", devHandler.GetClient)
|
||||||
dev.Get("/clients/:id", devHandler.GetClient)
|
api.Put("/clients/:id", devHandler.UpdateClient)
|
||||||
dev.Put("/clients/:id", devHandler.UpdateClient)
|
api.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
|
||||||
dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
|
api.Delete("/clients/:id", devHandler.DeleteClient)
|
||||||
dev.Delete("/clients/:id", devHandler.DeleteClient)
|
api.Get("/consents", devHandler.ListConsents)
|
||||||
dev.Get("/consents", devHandler.ListConsents)
|
api.Delete("/consents", devHandler.RevokeConsents)
|
||||||
dev.Delete("/consents", devHandler.RevokeConsents)
|
|
||||||
|
|
||||||
// Webhook for Descope Generic SMS Gateway
|
// Webhook for Descope Generic SMS Gateway
|
||||||
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)
|
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)
|
||||||
|
|||||||
@@ -18,16 +18,15 @@ const (
|
|||||||
// IdentityProviderConfig stores the configuration for an external Identity Provider.
|
// IdentityProviderConfig stores the configuration for an external Identity Provider.
|
||||||
type IdentityProviderConfig struct {
|
type IdentityProviderConfig struct {
|
||||||
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
|
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
|
||||||
TenantID string `gorm:"type:uuid;not null;index" json:"tenant_id"`
|
ClientID string `gorm:"type:uuid;not null;index" json:"client_id"` // Replaces TenantID
|
||||||
Tenant Tenant `gorm:"foreignKey:TenantID" json:"-"` // Belongs to Tenant
|
|
||||||
ProviderType ProviderType `gorm:"type:varchar(10);not null" json:"provider_type"`
|
ProviderType ProviderType `gorm:"type:varchar(10);not null" json:"provider_type"`
|
||||||
DisplayName string `gorm:"not null" json:"display_name"`
|
DisplayName string `gorm:"not null" json:"display_name"`
|
||||||
Status string `gorm:"default:'active'" json:"status"`
|
Status string `gorm:"default:'active'" json:"status"`
|
||||||
|
|
||||||
// OIDC Specific Fields
|
// OIDC Specific Fields
|
||||||
IssuerURL *string `gorm:"null" json:"issuer_url,omitempty"`
|
IssuerURL *string `gorm:"null" json:"issuer_url,omitempty"`
|
||||||
ClientID *string `gorm:"null" json:"client_id,omitempty"`
|
OIDCClientID *string `gorm:"null" json:"oidc_client_id,omitempty"` // Renamed from ClientID
|
||||||
ClientSecret *string `gorm:"null" json:"client_secret,omitempty"`
|
OIDCClientSecret *string `gorm:"null" json:"oidc_client_secret,omitempty"` // Renamed from ClientSecret
|
||||||
// Scopes are space-separated
|
// Scopes are space-separated
|
||||||
Scopes *string `gorm:"null" json:"scopes,omitempty"`
|
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)
|
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.
|
// ListIdpConfigsForTenant handles listing all IdP configurations for a tenant.
|
||||||
func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
|
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.
|
// This is a temporary solution. We should create a proper method in the repository.
|
||||||
var configs []domain.IdentityProviderConfig
|
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 {
|
if err := h.db.Where("tenant_id = ?", tenantID).Find(&configs).Error; err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
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"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation - This is the old validation logic
|
||||||
if req.TenantID == "" || req.DisplayName == "" || req.ProviderType == "" {
|
if req.ClientID == "" || req.DisplayName == "" || req.ProviderType == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id, display_name, and provider_type are required"})
|
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
|
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) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant not found"})
|
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)
|
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)
|
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)
|
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{
|
config := oauth2.Config{
|
||||||
ClientID: *provider.ClientID,
|
ClientID: *provider.OIDCClientID,
|
||||||
ClientSecret: *provider.ClientSecret,
|
ClientSecret: *provider.OIDCClientSecret,
|
||||||
Endpoint: oidcProvider.Endpoint(),
|
Endpoint: oidcProvider.Endpoint(),
|
||||||
RedirectURL: "http://localhost:8080/api/v1/federation/oidc/callback", // This should be configurable
|
RedirectURL: "http://localhost:8080/api/v1/federation/oidc/callback", // This should be configurable
|
||||||
Scopes: []string{*provider.Scopes},
|
Scopes: []string{*provider.Scopes},
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { createIdpConfig, listIdpConfigsForTenant } from "../../../lib/adminApi";
|
import {
|
||||||
import type { IdpConfigCreateRequest, IdpConfig } from "../../../lib/adminApi";
|
createIdpConfigForClient,
|
||||||
|
listIdpConfigsForClient,
|
||||||
|
} from "../../../lib/devApi";
|
||||||
|
import type { IdpConfigCreateRequest, IdpConfig } from "../../../lib/devApi";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
// Proper Modal Component with Form
|
// Proper Modal Component with Form
|
||||||
const CreateIdpModal = ({
|
const CreateIdpModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
tenantId,
|
clientId,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
tenantId: string;
|
clientId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [formData, setFormData] = useState<IdpConfigCreateRequest>({
|
const [formData, setFormData] = useState<IdpConfigCreateRequest>({
|
||||||
tenant_id: tenantId,
|
client_id: clientId,
|
||||||
provider_type: "oidc",
|
provider_type: "oidc",
|
||||||
display_name: "",
|
display_name: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
issuer_url: "",
|
issuer_url: "",
|
||||||
client_id: "",
|
oidc_client_id: "",
|
||||||
client_secret: "",
|
oidc_client_secret: "",
|
||||||
scopes: "openid email profile",
|
scopes: "openid email profile",
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (newData: IdpConfigCreateRequest) => createIdpConfig(newData),
|
mutationFn: (newData: IdpConfigCreateRequest) =>
|
||||||
|
createIdpConfigForClient(newData),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["idpConfigs", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["idpConfigs", clientId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -99,8 +103,8 @@ const CreateIdpModal = ({
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="client_id"
|
name="oidc_client_id"
|
||||||
value={formData.client_id}
|
value={formData.oidc_client_id}
|
||||||
onChange={handleChange}
|
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"
|
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
required
|
required
|
||||||
@@ -114,8 +118,8 @@ const CreateIdpModal = ({
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="client_secret"
|
name="oidc_client_secret"
|
||||||
value={formData.client_secret}
|
value={formData.oidc_client_secret}
|
||||||
onChange={handleChange}
|
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"
|
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
required
|
required
|
||||||
@@ -159,24 +163,24 @@ const CreateIdpModal = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TenantFederationPage() {
|
export function ClientFederationPage() {
|
||||||
const { tenantId } = useParams<{ tenantId: string }>();
|
const { id: clientId } = useParams<{ id: string }>();
|
||||||
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
if (!tenantId) {
|
if (!clientId) {
|
||||||
return <div>Tenant ID is missing</div>;
|
return <div>Client ID is missing</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["idpConfigs", tenantId],
|
queryKey: ["idpConfigs", clientId],
|
||||||
queryFn: () => listIdpConfigsForTenant(tenantId),
|
queryFn: () => listIdpConfigsForClient(clientId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h1 className="text-2xl font-bold mb-4">Identity Federation Settings</h1>
|
<h1 className="text-2xl font-bold mb-4">Identity Federation Settings</h1>
|
||||||
<p className="mb-4 text-gray-600">
|
<p className="mb-4 text-gray-600">
|
||||||
Manage external identity providers for this tenant.
|
Manage external identity providers for this application.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -191,7 +195,7 @@ export function TenantFederationPage() {
|
|||||||
<CreateIdpModal
|
<CreateIdpModal
|
||||||
isOpen={isCreateModalOpen}
|
isOpen={isCreateModalOpen}
|
||||||
onClose={() => setCreateModalOpen(false)}
|
onClose={() => setCreateModalOpen(false)}
|
||||||
tenantId={tenantId}
|
clientId={clientId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoading && <div>Loading configurations...</div>}
|
{isLoading && <div>Loading configurations...</div>}
|
||||||
@@ -59,6 +59,37 @@ export type ConsentListResponse = {
|
|||||||
items: ConsentSummary[];
|
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() {
|
export async function fetchClients() {
|
||||||
const { data } = await apiClient.get<ClientListResponse>("/clients");
|
const { data } = await apiClient.get<ClientListResponse>("/clients");
|
||||||
return data;
|
return data;
|
||||||
@@ -123,3 +154,36 @@ export async function revokeConsent(subject: string, clientId?: string) {
|
|||||||
}
|
}
|
||||||
await apiClient.delete("/consents", { params });
|
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