1
0
forked from baron/baron-sso

외부 OIDC IdP 연동 계획

This commit is contained in:
2026-01-29 10:28:13 +09:00
parent ec90853fe3
commit a8ac66b318
6 changed files with 196 additions and 133 deletions

View File

@@ -3,21 +3,80 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
"errors"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
// FederationHandler handles API requests for IdP federation configurations.
// FederationHandler handles API requests for IdP federation.
type FederationHandler struct {
Repo *repository.FederationRepository
DB *gorm.DB // For tenant existence checks
fedSvc *service.FederationService
repo repository.FederationRepository // For IdP Config CRUD
db *gorm.DB // For tenant existence checks, etc. in CRUD
}
// NewFederationHandler creates a new FederationHandler.
func NewFederationHandler(repo *repository.FederationRepository, db *gorm.DB) *FederationHandler {
return &FederationHandler{Repo: repo, DB: db}
func NewFederationHandler(fedSvc *service.FederationService, repo repository.FederationRepository, db *gorm.DB) *FederationHandler {
return &FederationHandler{
fedSvc: fedSvc,
repo: repo,
db: db,
}
}
// InitiateOIDCLogin handles the start of the OIDC login flow.
// It expects `provider_id` and `login_challenge` as query parameters.
func (h *FederationHandler) InitiateOIDCLogin(c *fiber.Ctx) error {
providerID := c.Query("provider_id")
loginChallenge := c.Query("login_challenge")
if providerID == "" || loginChallenge == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "provider_id and login_challenge are required"})
}
redirectURL, err := h.fedSvc.InitiateOIDCLogin(c.Context(), providerID, loginChallenge)
if err != nil {
// Log the error properly in a real application
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to initiate OIDC login"})
}
return c.Redirect(redirectURL, fiber.StatusFound)
}
// HandleOIDCCallback handles the OIDC callback from the IdP.
func (h *FederationHandler) HandleOIDCCallback(c *fiber.Ctx) error {
code := c.Query("code")
state := c.Query("state")
if code == "" || state == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "code and state are required"})
}
redirectURL, err := h.fedSvc.HandleOIDCCallback(c.Context(), code, state)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to handle OIDC callback"})
}
return c.Redirect(redirectURL, fiber.StatusFound)
}
// ListIdpConfigsForTenant handles listing all IdP configurations for a tenant.
func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
tenantID := c.Params("tenantId")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"})
}
// This is a temporary solution. We should create a proper method in the repository.
var configs []domain.IdentityProviderConfig
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.JSON(configs)
}
// CreateIdpConfig handles the creation of a new IdP configuration.
@@ -34,93 +93,18 @@ func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error {
// Check if tenant exists
var tenant domain.Tenant
if err := h.DB.First(&tenant, "id = ?", req.TenantID).Error; err != nil {
if err := h.db.First(&tenant, "id = ?", req.TenantID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if err := h.Repo.Create(&req); err != nil {
// 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)
}
// GetIdpConfigByID handles retrieving a single IdP configuration.
func (h *FederationHandler) GetIdpConfigByID(c *fiber.Ctx) error {
id := c.Params("id")
config, err := h.Repo.GetByID(id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if config == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "identity provider configuration not found"})
}
return c.JSON(config)
}
// ListIdpConfigsForTenant handles listing all IdP configurations for a tenant.
func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
tenantID := c.Params("tenantId")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"})
}
configs, err := h.Repo.ListByTenantID(tenantID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(configs)
}
// UpdateIdpConfig handles updating an IdP configuration.
func (h *FederationHandler) UpdateIdpConfig(c *fiber.Ctx) error {
id := c.Params("id")
existingConfig, err := h.Repo.GetByID(id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
if existingConfig == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "identity provider configuration not found"})
}
var req domain.IdentityProviderConfig
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
// Overwrite fields
existingConfig.DisplayName = req.DisplayName
existingConfig.Status = req.Status
existingConfig.IssuerURL = req.IssuerURL
existingConfig.ClientID = req.ClientID
existingConfig.ClientSecret = req.ClientSecret
existingConfig.Scopes = req.Scopes
existingConfig.MetadataURL = req.MetadataURL
existingConfig.MetadataXML = req.MetadataXML
existingConfig.EntityID = req.EntityID
existingConfig.AcsURL = req.AcsURL
if err := h.Repo.Update(existingConfig); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(existingConfig)
}
// DeleteIdpConfig handles deleting an IdP configuration.
func (h *FederationHandler) DeleteIdpConfig(c *fiber.Ctx) error {
id := c.Params("id")
if err := h.Repo.Delete(id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)
}
// TODO: Re-implement Update, Delete handlers for IdP Configs

View File

@@ -2,53 +2,9 @@ package repository
import (
"baron-sso-backend/internal/domain"
"errors"
"gorm.io/gorm"
"context"
)
// FederationRepository handles database operations for IdentityProviderConfig.
type FederationRepository struct {
DB *gorm.DB
}
// NewFederationRepository creates a new FederationRepository.
func NewFederationRepository(db *gorm.DB) *FederationRepository {
return &FederationRepository{DB: db}
}
// Create creates a new identity provider configuration.
func (r *FederationRepository) Create(config *domain.IdentityProviderConfig) error {
return r.DB.Create(config).Error
}
// GetByID retrieves an identity provider configuration by its ID.
func (r *FederationRepository) GetByID(id string) (*domain.IdentityProviderConfig, error) {
var config domain.IdentityProviderConfig
if err := r.DB.First(&config, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // Return nil, nil for not found
}
return nil, err
}
return &config, nil
}
// ListByTenantID retrieves all identity provider configurations for a given tenant.
func (r *FederationRepository) ListByTenantID(tenantID string) ([]domain.IdentityProviderConfig, error) {
var configs []domain.IdentityProviderConfig
if err := r.DB.Where("tenant_id = ?", tenantID).Find(&configs).Error; err != nil {
return nil, err
}
return configs, nil
}
// Update updates an existing identity provider configuration.
func (r *FederationRepository) Update(config *domain.IdentityProviderConfig) error {
return r.DB.Save(config).Error
}
// Delete removes an identity provider configuration by its ID.
func (r *FederationRepository) Delete(id string) error {
return r.DB.Delete(&domain.IdentityProviderConfig{}, "id = ?", id).Error
}
type FederationRepository interface {
FindProviderByID(ctx context.Context, providerID string) (*domain.IdentityProviderConfig, error)
}

View File

@@ -0,0 +1,23 @@
package repository
import (
"baron-sso-backend/internal/domain"
"context"
"gorm.io/gorm"
)
type GormFederationRepository struct {
db *gorm.DB
}
func NewGormFederationRepository(db *gorm.DB) *GormFederationRepository {
return &GormFederationRepository{db: db}
}
func (r *GormFederationRepository) FindProviderByID(ctx context.Context, providerID string) (*domain.IdentityProviderConfig, error) {
var provider domain.IdentityProviderConfig
if err := r.db.WithContext(ctx).First(&provider, "id = ?", providerID).Error; err != nil {
return nil, err
}
return &provider, nil
}

View File

@@ -0,0 +1,91 @@
package service
import (
"baron-sso-backend/internal/repository"
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"time"
"golang.org/x/oauth2"
"github.com/coreos/go-oidc/v3/oidc"
)
type FederationService struct {
repo repository.FederationRepository
hydraSvc *HydraAdminService
redisSvc *RedisService
}
func NewFederationService(repo repository.FederationRepository, hydraSvc *HydraAdminService, redisSvc *RedisService) *FederationService {
return &FederationService{repo: repo, hydraSvc: hydraSvc, redisSvc: redisSvc}
}
func (s *FederationService) InitiateOIDCLogin(ctx context.Context, providerID, loginChallenge string) (string, error) {
provider, err := s.repo.FindProviderByID(ctx, providerID)
if err != nil {
return "", fmt.Errorf("failed to find provider: %w", err)
}
if provider == nil || provider.IssuerURL == nil || provider.ClientID == nil || provider.ClientSecret == nil || provider.Scopes == nil {
return "", fmt.Errorf("OIDC configuration for provider %s is incomplete", providerID)
}
oidcProvider, err := oidc.NewProvider(ctx, *provider.IssuerURL)
if err != nil {
return "", fmt.Errorf("failed to create OIDC provider: %w", err)
}
config := oauth2.Config{
ClientID: *provider.ClientID,
ClientSecret: *provider.ClientSecret,
Endpoint: oidcProvider.Endpoint(),
RedirectURL: "http://localhost:8080/api/v1/federation/oidc/callback", // This should be configurable
Scopes: []string{*provider.Scopes},
}
state, err := generateState()
if err != nil {
return "", fmt.Errorf("failed to generate state: %w", err)
}
// Store state and login_challenge in Redis
redisKey := fmt.Sprintf("oidc_state:%s", state)
if err := s.redisSvc.Set(redisKey, loginChallenge, 10*time.Minute); err != nil {
return "", fmt.Errorf("failed to save state to Redis: %w", err)
}
return config.AuthCodeURL(state), nil
}
func (s *FederationService) HandleOIDCCallback(ctx context.Context, code, state string) (string, error) {
// 1. Retrieve login_challenge from Redis
redisKey := fmt.Sprintf("oidc_state:%s", state)
loginChallenge, err := s.redisSvc.Get(redisKey)
if err != nil {
return "", fmt.Errorf("failed to get state from Redis or state expired: %w", err)
}
// Delete the state from Redis now that it's been used
s.redisSvc.Delete(redisKey)
// TODO: Finish the rest of the callback logic
// 2. Exchange code for token
// 3. Verify ID token
// 4. JIT Provisioning
// 5. Accept Hydra Login Request
fmt.Println("Login challenge found:", loginChallenge)
return "http://localhost:3000/login?login_successful=true", nil // Placeholder
}
func generateState() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}