forked from baron/baron-sso
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -91,6 +91,7 @@ type AuthHandler struct {
|
||||
OathkeeperRepo domain.OathkeeperLogRepository
|
||||
Hydra *service.HydraAdminService
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
UserRepo repository.UserRepository
|
||||
}
|
||||
|
||||
@@ -149,7 +150,7 @@ func checkPollInterval(redis *service.RedisService, key string, interval time.Du
|
||||
return false, int(interval.Seconds())
|
||||
}
|
||||
|
||||
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, userRepo repository.UserRepository) *AuthHandler {
|
||||
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, userRepo repository.UserRepository) *AuthHandler {
|
||||
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
||||
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
||||
|
||||
@@ -177,6 +178,7 @@ func NewAuthHandler(redisService *service.RedisService, idpProvider domain.Ident
|
||||
OathkeeperRepo: oathkeeperRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
UserRepo: userRepo,
|
||||
}
|
||||
}
|
||||
@@ -405,16 +407,26 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Identity provider unavailable"})
|
||||
}
|
||||
|
||||
// [New] Auto-Assign Tenant by Domain
|
||||
companyCode := req.CompanyCode
|
||||
if companyCode == "" {
|
||||
parts := strings.Split(req.Email, "@")
|
||||
if len(parts) == 2 {
|
||||
domainName := parts[1]
|
||||
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), domainName)
|
||||
if err == nil && tenant != nil {
|
||||
// [Strict] Enforce Tenant Auto-Assignment by Domain ONLY
|
||||
// Manual companyCode from request is ignored to prevent unauthorized tenant joining
|
||||
companyCode := ""
|
||||
var tenantID *string
|
||||
|
||||
parts := strings.Split(req.Email, "@")
|
||||
if len(parts) == 2 {
|
||||
domainName := parts[1]
|
||||
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), domainName)
|
||||
if err == nil && tenant != nil {
|
||||
if tenant.Status == domain.TenantStatusActive {
|
||||
slog.Info("[Signup] Auto-assigning tenant", "email", req.Email, "tenant", tenant.Slug)
|
||||
companyCode = tenant.Slug
|
||||
tenantID = &tenant.ID
|
||||
} else {
|
||||
slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
|
||||
// Policy: If tenant exists but not active, reject signup or allow as general?
|
||||
// For now, let's allow as general but log it.
|
||||
// Or return error if we want strict domain locking.
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,21 +481,27 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
Phone: normalizedPhone,
|
||||
AffiliationType: req.AffiliationType,
|
||||
CompanyCode: companyCode,
|
||||
TenantID: tenantID,
|
||||
Department: req.Department,
|
||||
Role: "user",
|
||||
Status: "active",
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
// Link TenantID if possible
|
||||
if companyCode != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), companyCode); err == nil && tenant != nil {
|
||||
localUser.TenantID = &tenant.ID
|
||||
if h.UserRepo != nil {
|
||||
if err := h.UserRepo.Create(c.Context(), localUser); err != nil {
|
||||
slog.Error("[Signup] Failed to sync user to local DB", "email", req.Email, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.UserRepo.Create(c.Context(), localUser); err != nil {
|
||||
slog.Error("[Signup] Failed to sync user to local DB", "email", req.Email, "error", err)
|
||||
// We don't fail the whole signup if local sync fails
|
||||
// [Keto] Sync user-tenant relationship
|
||||
if h.KetoService != nil && tenantID != nil {
|
||||
go func() {
|
||||
err := h.KetoService.CreateRelation(context.Background(), "Tenant", *tenantID, "members", providerID)
|
||||
if err != nil {
|
||||
slog.Error("[Signup] Failed to sync membership to Keto", "userID", providerID, "tenantID", *tenantID, "error", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
@@ -2555,69 +2573,18 @@ func (h *AuthHandler) formatPhoneForStorage(phone string) string {
|
||||
return phone
|
||||
}
|
||||
|
||||
// GetMe - Returns current user's profile with 010 phone format
|
||||
// GetMe - Returns current user's profile with enriched data from local DB
|
||||
func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
userResponse, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load user profile"})
|
||||
}
|
||||
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
||||
}
|
||||
|
||||
dept, _ := userResponse.CustomAttributes["department"].(string)
|
||||
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
||||
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
||||
|
||||
resp := domain.UserProfileResponse{
|
||||
ID: identityID,
|
||||
Email: userResponse.Email,
|
||||
Name: userResponse.Name,
|
||||
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
||||
Department: dept,
|
||||
AffiliationType: affType,
|
||||
CompanyCode: compCode,
|
||||
Metadata: userResponse.CustomAttributes,
|
||||
}
|
||||
|
||||
if compCode != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), compCode); err == nil && tenant != nil {
|
||||
resp.Tenant = tenant
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
}
|
||||
|
||||
profile, err := h.getKratosProfile(token)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
||||
}
|
||||
return c.JSON(profile)
|
||||
}
|
||||
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing authorization token"})
|
||||
}
|
||||
profile, err := h.getKratosProfileWithCookie(cookie)
|
||||
profile, err := h.resolveCurrentProfile(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(profile)
|
||||
}
|
||||
// GetEnrichedProfile - Exported wrapper for resolveCurrentProfile used by middlewares
|
||||
func (h *AuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
return h.resolveCurrentProfile(c)
|
||||
}
|
||||
|
||||
func looksLikeJWT(token string) bool {
|
||||
return strings.Count(token, ".") == 2
|
||||
@@ -3539,52 +3506,101 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
// [Development Mode Fallback]
|
||||
if os.Getenv("APP_ENV") != "production" {
|
||||
// 우선순위: 1. 헤더, 2. 쿠키, 3. 기본값(user)
|
||||
testRole := c.Get("X-Test-Role")
|
||||
if testRole == "" {
|
||||
testRole = c.Cookies("X-Mock-Role")
|
||||
}
|
||||
|
||||
if testRole == "" {
|
||||
testRole = domain.RoleUser // 기본값을 user로 변경하여 차단 확인
|
||||
}
|
||||
|
||||
slog.Info("Using MOCK profile", "role", testRole, "source", "dev_fallback")
|
||||
return &domain.UserProfileResponse{
|
||||
ID: "dev-admin-uuid",
|
||||
Email: "dev-admin@baron.local",
|
||||
Name: "Dev Admin (" + testRole + ")",
|
||||
Role: testRole,
|
||||
CompanyCode: "hanmac",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var profile *domain.UserProfileResponse
|
||||
var err error
|
||||
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
if looksLikeJWT(token) && h.DescopeClient != nil {
|
||||
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
||||
if err == nil && authorized {
|
||||
userResponse, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err == nil {
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr == nil && identityID != "" {
|
||||
dept, _ := userResponse.CustomAttributes["department"].(string)
|
||||
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
||||
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
||||
profile = &domain.UserProfileResponse{
|
||||
ID: identityID,
|
||||
Email: userResponse.Email,
|
||||
Name: userResponse.Name,
|
||||
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
||||
Department: dept,
|
||||
AffiliationType: affType,
|
||||
CompanyCode: compCode,
|
||||
Metadata: userResponse.CustomAttributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
identityID, resolveErr := h.resolveKratosIdentityID(
|
||||
c.Context(),
|
||||
userResponse.Email,
|
||||
normalizePhoneForLoginID(userResponse.Phone),
|
||||
)
|
||||
if resolveErr != nil || identityID == "" {
|
||||
return nil, fmt.Errorf("failed to resolve kratos identity for profile")
|
||||
}
|
||||
dept, _ := userResponse.CustomAttributes["department"].(string)
|
||||
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
||||
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
||||
return &domain.UserProfileResponse{
|
||||
ID: identityID,
|
||||
Email: userResponse.Email,
|
||||
Name: userResponse.Name,
|
||||
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
||||
Department: dept,
|
||||
AffiliationType: affType,
|
||||
CompanyCode: compCode,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
profile, err := h.getKratosProfile(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if profile == nil {
|
||||
profile, err = h.getKratosProfile(token)
|
||||
}
|
||||
} else {
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie != "" {
|
||||
profile, err = h.getKratosProfileWithCookie(cookie)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie == "" {
|
||||
return nil, fmt.Errorf("missing authorization token")
|
||||
if err != nil || profile == nil {
|
||||
return nil, errors.New("invalid session")
|
||||
}
|
||||
return h.getKratosProfileWithCookie(cookie)
|
||||
|
||||
// [New] Enrich with Local DB (Roles, TenantID, etc.)
|
||||
if h.UserRepo != nil {
|
||||
localUser, err := h.UserRepo.FindByID(c.Context(), profile.ID)
|
||||
if err == nil && localUser != nil {
|
||||
profile.Role = localUser.Role
|
||||
profile.TenantID = localUser.TenantID
|
||||
profile.RelyingPartyID = localUser.RelyingPartyID
|
||||
if profile.Tenant == nil && localUser.Tenant != nil {
|
||||
profile.Tenant = localUser.Tenant
|
||||
}
|
||||
} else {
|
||||
// 로컬 DB에 없으면 기본 권한 부여
|
||||
profile.Role = domain.RoleUser
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 DB에 Tenant 정보가 없더라도 companyCode(slug)가 있으면 조회 시도
|
||||
if profile.Tenant == nil && profile.CompanyCode != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), profile.CompanyCode); err == nil && tenant != nil {
|
||||
profile.Tenant = tenant
|
||||
}
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
|
||||
Reference in New Issue
Block a user