forked from baron/baron-sso
외부 OIDC IdP 연동 계획
This commit is contained in:
@@ -36,10 +36,12 @@ require (
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -72,6 +74,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
|
||||
@@ -38,6 +38,8 @@ github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgIS
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -53,6 +55,8 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
@@ -184,6 +188,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
23
backend/internal/repository/gorm_federation_repository.go
Normal file
23
backend/internal/repository/gorm_federation_repository.go
Normal 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
|
||||
}
|
||||
91
backend/internal/service/federation_service.go
Normal file
91
backend/internal/service/federation_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user