forked from baron/baron-sso
조직도 M2M조회 추가, 자동로그인 보완
This commit is contained in:
@@ -704,7 +704,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Identity provider unavailable")
|
||||
}
|
||||
|
||||
// [New Policy] Enforce Explicit Tenant Assignment (No Auto-Provisioning)
|
||||
// 소속이 비어 있는 일반 가입자는 PERSONAL tenant를 자동 생성해 대표소속을 보장합니다.
|
||||
companyCode := ""
|
||||
var tenantID *string
|
||||
|
||||
@@ -765,6 +765,14 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
if tenantID == nil && req.AffiliationType == "AFFILIATE" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "We couldn't verify your organization affiliation. Please check your choice.")
|
||||
}
|
||||
if tenantID == nil && req.AffiliationType == "GENERAL" {
|
||||
tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant")
|
||||
}
|
||||
companyCode = tenant.Slug
|
||||
tenantID = &tenant.ID
|
||||
}
|
||||
|
||||
// Normalize Phone (E.164 형태로 보관)
|
||||
normalizedPhone := strings.ReplaceAll(req.Phone, "-", "")
|
||||
@@ -785,6 +793,9 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
"grade": "",
|
||||
"role": domain.RoleUser,
|
||||
}
|
||||
if tenantID != nil {
|
||||
attributes["tenant_id"] = *tenantID
|
||||
}
|
||||
|
||||
// Sync all custom login IDs based on tenant schemas
|
||||
loginIDRecords := syncCustomLoginIDs(c.Context(), h.TenantService, attributes, req.Metadata, "")
|
||||
@@ -1100,6 +1111,10 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID
|
||||
if traits == nil {
|
||||
return claims
|
||||
}
|
||||
if tenantID == "" {
|
||||
tenantID = representativeTenantIDFromTraits(traits)
|
||||
}
|
||||
includeTenantDetails := tenantClaimScopeRequested(scopes)
|
||||
|
||||
scopeSet := map[string]struct{}{}
|
||||
for _, scope := range scopes {
|
||||
@@ -1200,53 +1215,38 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID
|
||||
// [New] Dynamic Claim Injection for Multi-tenancy
|
||||
if tenantID != "" {
|
||||
claims["tenant_id"] = tenantID
|
||||
// Extract namespaced metadata if available
|
||||
// The key in traits is expected to be the tenantID
|
||||
if namespaced, ok := traits[tenantID].(map[string]any); ok {
|
||||
for k, v := range namespaced {
|
||||
claims[k] = v
|
||||
}
|
||||
} else if namespaced, ok := traits[tenantID].(map[string]interface{}); ok {
|
||||
for k, v := range namespaced {
|
||||
claims[k] = v
|
||||
if includeTenantDetails {
|
||||
// tenant 스코프가 있을 때만 대표소속 namespace metadata를 top-level claim으로 펼칩니다.
|
||||
if namespaced, ok := traits[tenantID].(map[string]any); ok {
|
||||
for k, v := range namespaced {
|
||||
claims[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Update] Pass ALL tenants the user belongs to
|
||||
allTenants := map[string]any{}
|
||||
var joinedTenants []string
|
||||
joinedTenants := joinedTenantIDsFromTraits(traits, tenantID)
|
||||
|
||||
// Heuristic: if a trait value is a map, it's treated as namespaced metadata for a tenant
|
||||
for k, v := range traits {
|
||||
if k == "metadata" {
|
||||
continue
|
||||
}
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
allTenants[k] = m
|
||||
joinedTenants = append(joinedTenants, k)
|
||||
} else if m, ok := v.(map[string]interface{}); ok {
|
||||
allTenants[k] = m
|
||||
joinedTenants = append(joinedTenants, k)
|
||||
}
|
||||
}
|
||||
|
||||
// [Fix] Include primary tenant_id in joined_tenants if it's not already there
|
||||
if primaryTenantID := getString("tenant_id"); primaryTenantID != "" {
|
||||
found := false
|
||||
for _, id := range joinedTenants {
|
||||
if id == primaryTenantID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
joinedTenants = append(joinedTenants, primaryTenantID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(allTenants) > 0 || len(joinedTenants) > 0 {
|
||||
claims["tenants"] = allTenants
|
||||
if len(joinedTenants) > 0 {
|
||||
claims["joined_tenants"] = joinedTenants
|
||||
}
|
||||
|
||||
if includeTenantDetails && len(allTenants) > 0 {
|
||||
claims["tenants"] = allTenants
|
||||
}
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
@@ -1268,6 +1268,311 @@ func composeOIDCSessionClaims(client domain.HydraClient, traits map[string]any,
|
||||
return withOidcSessionMetadata(claims, sessionID)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) withHanmacFamilyTenantClaims(ctx context.Context, claims map[string]any, traits map[string]any, scopes []string) map[string]any {
|
||||
if claims == nil {
|
||||
claims = map[string]any{}
|
||||
}
|
||||
if h == nil || h.TenantService == nil {
|
||||
return claims
|
||||
}
|
||||
|
||||
appointments := tenantClaimAppointmentsFromTraits(traits)
|
||||
includeTenantDetails := tenantClaimScopeRequested(scopes)
|
||||
tenants, hadTenantClaims := claims["tenants"].(map[string]any)
|
||||
if !hadTenantClaims {
|
||||
tenants = map[string]any{}
|
||||
}
|
||||
createdTenantClaims := map[string]bool{}
|
||||
if tenantID := tenantClaimString(claims, "tenant_id"); tenantID != "" {
|
||||
if _, exists := tenants[tenantID]; !exists {
|
||||
tenants[tenantID] = map[string]any{}
|
||||
createdTenantClaims[tenantID] = true
|
||||
}
|
||||
}
|
||||
for _, tenantKey := range tenantClaimAppointmentPrimaryKeys(appointments) {
|
||||
if _, exists := tenants[tenantKey]; !exists {
|
||||
tenants[tenantKey] = map[string]any{}
|
||||
createdTenantClaims[tenantKey] = true
|
||||
}
|
||||
}
|
||||
if len(tenants) == 0 {
|
||||
return claims
|
||||
}
|
||||
|
||||
leadTenantIDs := make([]string, 0)
|
||||
joinedTenantIDs := make([]string, 0)
|
||||
|
||||
for tenantKey, rawTenantClaim := range tenants {
|
||||
tenantClaim, ok := rawTenantClaim.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
tenant, ancestors, inHanmacFamily := h.resolveHanmacFamilyTenantClaimAncestry(ctx, tenantKey)
|
||||
if !inHanmacFamily || tenant == nil {
|
||||
if createdTenantClaims[tenantKey] {
|
||||
delete(tenants, tenantKey)
|
||||
}
|
||||
continue
|
||||
}
|
||||
joinedTenantIDs = append(joinedTenantIDs, tenant.ID)
|
||||
if !includeTenantDetails {
|
||||
if createdTenantClaims[tenantKey] {
|
||||
delete(tenants, tenantKey)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
tenantClaim["id"] = tenant.ID
|
||||
tenantClaim["slug"] = tenant.Slug
|
||||
tenantClaim["name"] = tenant.Name
|
||||
tenantClaim["type"] = tenant.Type
|
||||
tenantClaim["ancestors"] = ancestors
|
||||
if len(ancestors) > 0 {
|
||||
tenantClaim["parentTenantId"] = ancestors[0]["id"]
|
||||
} else {
|
||||
tenantClaim["parentTenantId"] = nil
|
||||
}
|
||||
delete(tenantClaim, "parentTenant")
|
||||
|
||||
if appointment := lookupTenantClaimAppointment(appointments, tenantKey, tenant); appointment != nil {
|
||||
mergeTenantAppointmentClaim(tenantClaim, appointment)
|
||||
}
|
||||
|
||||
if lead, ok := metadataBoolFromMap(tenantClaim, "lead", "isLead", "isOwner", "isManager"); ok {
|
||||
tenantClaim["lead"] = lead
|
||||
if lead {
|
||||
leadTenantIDs = append(leadTenantIDs, tenant.ID)
|
||||
}
|
||||
}
|
||||
if representative, ok := metadataBoolFromMap(tenantClaim, "representative", "isPrimary", "primary"); ok {
|
||||
tenantClaim["representative"] = representative
|
||||
tenantClaim["isPrimary"] = representative
|
||||
}
|
||||
|
||||
tenants[tenantKey] = tenantClaim
|
||||
}
|
||||
|
||||
if len(leadTenantIDs) > 0 {
|
||||
claims["lead_tenants"] = uniqueSortedStrings(leadTenantIDs)
|
||||
}
|
||||
if len(joinedTenantIDs) > 0 {
|
||||
claims["joined_tenants"] = mergeClaimStringList(claims["joined_tenants"], joinedTenantIDs)
|
||||
}
|
||||
if !includeTenantDetails {
|
||||
if !hadTenantClaims {
|
||||
delete(claims, "tenants")
|
||||
}
|
||||
delete(claims, "lead_tenants")
|
||||
return claims
|
||||
}
|
||||
if len(tenants) > 0 {
|
||||
claims["tenants"] = tenants
|
||||
} else if !hadTenantClaims {
|
||||
delete(claims, "tenants")
|
||||
}
|
||||
return claims
|
||||
}
|
||||
|
||||
func tenantClaimScopeRequested(scopes []string) bool {
|
||||
for _, scope := range scopes {
|
||||
if strings.EqualFold(strings.TrimSpace(scope), "tenant") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeClaimStringList(raw any, values []string) []string {
|
||||
merged := make([]string, 0, len(values))
|
||||
switch current := raw.(type) {
|
||||
case []string:
|
||||
merged = append(merged, current...)
|
||||
case []any:
|
||||
for _, item := range current {
|
||||
if s, ok := item.(string); ok {
|
||||
merged = append(merged, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
merged = append(merged, values...)
|
||||
return uniqueSortedStrings(merged)
|
||||
}
|
||||
|
||||
func tenantClaimAppointmentPrimaryKeys(appointments map[string]map[string]any) []string {
|
||||
if len(appointments) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
keys := make([]string, 0, len(appointments))
|
||||
for _, appointment := range appointments {
|
||||
for _, key := range []string{"tenantId", "tenant_id", "tenantSlug", "tenant_slug"} {
|
||||
value := tenantClaimString(appointment, key)
|
||||
if value == "" || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
keys = append(keys, value)
|
||||
break
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func tenantClaimAppointmentsFromTraits(traits map[string]any) map[string]map[string]any {
|
||||
raw := rawAdditionalAppointments(traits)
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
items, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
appointments := make(map[string]map[string]any)
|
||||
for _, item := range items {
|
||||
appointment, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, key := range []string{"tenantId", "tenant_id", "tenantSlug", "tenant_slug"} {
|
||||
if id := tenantClaimString(appointment, key); id != "" {
|
||||
appointments[id] = appointment
|
||||
}
|
||||
}
|
||||
}
|
||||
return appointments
|
||||
}
|
||||
|
||||
func rawAdditionalAppointments(traits map[string]any) any {
|
||||
if traits == nil {
|
||||
return nil
|
||||
}
|
||||
if raw, ok := traits["additionalAppointments"]; ok {
|
||||
return raw
|
||||
}
|
||||
if metadata, ok := traits["metadata"].(map[string]any); ok {
|
||||
return metadata["additionalAppointments"]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupTenantClaimAppointment(appointments map[string]map[string]any, tenantKey string, tenant *domain.Tenant) map[string]any {
|
||||
if len(appointments) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, key := range []string{tenantKey, tenant.ID, tenant.Slug} {
|
||||
if appointment, ok := appointments[key]; ok {
|
||||
return appointment
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeTenantAppointmentClaim(tenantClaim map[string]any, appointment map[string]any) {
|
||||
for _, key := range []string{"grade", "jobTitle", "job_title", "position"} {
|
||||
if value := tenantClaimString(appointment, key); value != "" {
|
||||
switch key {
|
||||
case "job_title":
|
||||
tenantClaim["jobTitle"] = value
|
||||
default:
|
||||
tenantClaim[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if lead, ok := metadataBoolFromMap(appointment, "lead", "isLead", "isOwner", "isManager"); ok {
|
||||
tenantClaim["lead"] = lead
|
||||
}
|
||||
if representative, ok := metadataBoolFromMap(appointment, "representative", "isPrimary", "primary"); ok {
|
||||
tenantClaim["representative"] = representative
|
||||
tenantClaim["isPrimary"] = representative
|
||||
}
|
||||
}
|
||||
|
||||
func tenantClaimString(values map[string]any, key string) string {
|
||||
raw, ok := values[key]
|
||||
if !ok || raw == nil {
|
||||
return ""
|
||||
}
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(value)
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(value))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveHanmacFamilyTenantClaimAncestry(ctx context.Context, identifier string) (*domain.Tenant, []map[string]any, bool) {
|
||||
tenant, err := h.resolveTenantClaimTenant(ctx, identifier)
|
||||
if err != nil || tenant == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
if strings.EqualFold(tenant.Slug, hanmacFamilyTenantSlug) {
|
||||
return tenant, []map[string]any{}, true
|
||||
}
|
||||
|
||||
ancestors := make([]*domain.Tenant, 0)
|
||||
visited := map[string]bool{tenant.ID: true}
|
||||
current := tenant
|
||||
for current.ParentID != nil && strings.TrimSpace(*current.ParentID) != "" {
|
||||
parentID := strings.TrimSpace(*current.ParentID)
|
||||
if visited[parentID] {
|
||||
return tenant, tenantClaimAncestorSummaries(ancestors), false
|
||||
}
|
||||
visited[parentID] = true
|
||||
|
||||
parent, err := h.TenantService.GetTenant(ctx, parentID)
|
||||
if err != nil || parent == nil {
|
||||
return tenant, tenantClaimAncestorSummaries(ancestors), false
|
||||
}
|
||||
ancestors = append(ancestors, parent)
|
||||
if strings.EqualFold(parent.Slug, hanmacFamilyTenantSlug) {
|
||||
return tenant, tenantClaimAncestorSummaries(ancestors), true
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
|
||||
return tenant, tenantClaimAncestorSummaries(ancestors), false
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveTenantClaimTenant(ctx context.Context, identifier string) (*domain.Tenant, error) {
|
||||
identifier = strings.TrimSpace(identifier)
|
||||
if identifier == "" {
|
||||
return nil, errors.New("tenant identifier is required")
|
||||
}
|
||||
if tenant, err := h.TenantService.GetTenant(ctx, identifier); err == nil && tenant != nil {
|
||||
return tenant, nil
|
||||
}
|
||||
return h.TenantService.GetTenantBySlug(ctx, identifier)
|
||||
}
|
||||
|
||||
func tenantClaimTenantSummary(tenant *domain.Tenant) map[string]any {
|
||||
return map[string]any{
|
||||
"id": tenant.ID,
|
||||
"slug": tenant.Slug,
|
||||
"name": tenant.Name,
|
||||
"type": tenant.Type,
|
||||
}
|
||||
}
|
||||
|
||||
func tenantClaimAncestorSummaries(ancestors []*domain.Tenant) []map[string]any {
|
||||
if len(ancestors) == 0 {
|
||||
return []map[string]any{}
|
||||
}
|
||||
items := make([]map[string]any, 0, len(ancestors))
|
||||
for i, ancestor := range ancestors {
|
||||
item := tenantClaimTenantSummary(ancestor)
|
||||
if i+1 < len(ancestors) {
|
||||
item["parentTenantId"] = ancestors[i+1].ID
|
||||
} else {
|
||||
item["parentTenantId"] = nil
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func applyConfiguredIDTokenClaims(baseClaims map[string]any, metadata map[string]interface{}) map[string]any {
|
||||
if baseClaims == nil {
|
||||
baseClaims = map[string]any{}
|
||||
@@ -5535,19 +5840,14 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), consentRequest.Subject)
|
||||
if err == nil && identity != nil {
|
||||
currentSessionID := h.resolveCurrentSessionID(c)
|
||||
var tenantID string
|
||||
if consentRequest.Client.Metadata != nil {
|
||||
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
|
||||
tenantID = tid
|
||||
}
|
||||
}
|
||||
sessionClaims := composeOIDCSessionClaims(
|
||||
consentRequest.Client,
|
||||
identity.Traits,
|
||||
consentRequest.RequestedScope,
|
||||
tenantID,
|
||||
representativeTenantIDFromTraits(identity.Traits),
|
||||
currentSessionID,
|
||||
)
|
||||
sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope)
|
||||
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
|
||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims)
|
||||
if err == nil {
|
||||
@@ -5571,10 +5871,10 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
// 신원 정보를 가져오지 못하면 자동 승인을 진행할 수 없으므로 일반 흐름(UI 노출)으로 진행
|
||||
} else {
|
||||
currentSessionID := h.resolveCurrentSessionID(c)
|
||||
var tenantID string
|
||||
var clientTenantID string
|
||||
if consentRequest.Client.Metadata != nil {
|
||||
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
|
||||
tenantID = tid
|
||||
clientTenantID = tid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5582,9 +5882,10 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
consentRequest.Client,
|
||||
identity.Traits,
|
||||
consentRequest.RequestedScope,
|
||||
tenantID,
|
||||
representativeTenantIDFromTraits(identity.Traits),
|
||||
currentSessionID,
|
||||
)
|
||||
sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope)
|
||||
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
|
||||
|
||||
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용 - 자동 승인 시)
|
||||
@@ -5627,7 +5928,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
EventID: GenerateSecureToken(16),
|
||||
Timestamp: time.Now(),
|
||||
UserID: consentRequest.Subject,
|
||||
TenantID: tenantID, // Uses the tenantID extracted earlier
|
||||
TenantID: clientTenantID,
|
||||
SessionID: currentSessionID,
|
||||
EventType: "consent.granted",
|
||||
Status: "success",
|
||||
@@ -5761,11 +6062,10 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
c.Locals("login_id", loginID)
|
||||
}
|
||||
currentSessionID := h.resolveCurrentSessionID(c)
|
||||
|
||||
var tenantID string
|
||||
var clientTenantID string
|
||||
if consentRequest.Client.Metadata != nil {
|
||||
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
|
||||
tenantID = tid
|
||||
clientTenantID = tid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5773,9 +6073,10 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
consentRequest.Client,
|
||||
identity.Traits,
|
||||
consentRequest.RequestedScope,
|
||||
tenantID,
|
||||
representativeTenantIDFromTraits(identity.Traits),
|
||||
currentSessionID,
|
||||
)
|
||||
sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope)
|
||||
sessionClaims = h.withRPProfileClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject)
|
||||
|
||||
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용)
|
||||
@@ -5821,7 +6122,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
EventID: GenerateSecureToken(16),
|
||||
Timestamp: time.Now(),
|
||||
UserID: consentRequest.Subject,
|
||||
TenantID: tenantID, // [New] Add TenantID to AuditLog
|
||||
TenantID: clientTenantID,
|
||||
SessionID: currentSessionID,
|
||||
EventType: "consent.granted",
|
||||
Status: "success",
|
||||
|
||||
Reference in New Issue
Block a user