forked from baron/baron-sso
feat: support dynamic multi-tenant OIDC claims injection (#609)
- Inject claim based on OIDC Client metadata - Extract namespaced tenant metadata from traits and flatten it to root - Expose all joined tenants metadata under and arrays - Fix missing AuditLog generation during auto-accepted Consent - Associate correct during auth events AuditLog recording - Add unit and integration tests for dynamic claims
This commit is contained in:
@@ -990,7 +990,7 @@ func normalizePhoneForLoginID(phone string) string {
|
||||
return normalized
|
||||
}
|
||||
|
||||
func buildOidcClaimsFromTraits(traits map[string]any, scopes []string) map[string]any {
|
||||
func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID string) map[string]any {
|
||||
claims := map[string]any{}
|
||||
if traits == nil {
|
||||
return claims
|
||||
@@ -1092,9 +1092,58 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string) map[strin
|
||||
}
|
||||
}
|
||||
|
||||
// [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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Update] Pass ALL tenants the user belongs to
|
||||
allTenants := map[string]any{}
|
||||
var joinedTenants []string
|
||||
|
||||
// Heuristic: if a trait value is a map, it's treated as namespaced metadata for a tenant
|
||||
for k, v := range traits {
|
||||
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
|
||||
claims["joined_tenants"] = joinedTenants
|
||||
}
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
func withOidcSessionMetadata(claims map[string]any, sessionID string) map[string]any {
|
||||
if claims == nil {
|
||||
claims = map[string]any{}
|
||||
@@ -2957,6 +3006,12 @@ func attachAuditClientDetails(c *fiber.Ctx, client domain.HydraClient) {
|
||||
clientName = clientID
|
||||
}
|
||||
|
||||
if client.Metadata != nil {
|
||||
if tid, ok := client.Metadata["tenant_id"].(string); ok && tid != "" {
|
||||
c.Locals("tenant_id", tid)
|
||||
}
|
||||
}
|
||||
|
||||
c.Locals("audit_details_extra", map[string]any{
|
||||
"client_id": clientID,
|
||||
"client_name": clientName,
|
||||
@@ -5081,10 +5136,23 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
slog.Error("failed to load identity for skip consent", "error", err, "subject", consentRequest.Subject)
|
||||
// 신원 정보를 가져오지 못하면 자동 승인을 진행할 수 없으므로 일반 흐름(UI 노출)으로 진행
|
||||
} else {
|
||||
var tenantID string
|
||||
if consentRequest.Client.Metadata != nil {
|
||||
if tid, ok := consentRequest.Client.Metadata["tenant_id"].(string); ok {
|
||||
tenantID = tid
|
||||
}
|
||||
}
|
||||
|
||||
sessionClaims := withOidcSessionMetadata(
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope),
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID),
|
||||
h.resolveCurrentSessionID(c),
|
||||
)
|
||||
|
||||
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용 - 자동 승인 시)
|
||||
if debugClaimsJSON, err := json.MarshalIndent(sessionClaims, "", " "); err == nil {
|
||||
slog.Info("=== [ACTUAL DATA] GENERATED OIDC CLAIMS (SKIP) ===", "claims", string(debugClaimsJSON))
|
||||
}
|
||||
|
||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims)
|
||||
if err != nil {
|
||||
slog.Error("failed to auto-accept hydra consent request", "error", err)
|
||||
@@ -5099,6 +5167,35 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
}
|
||||
_ = h.ConsentRepo.Upsert(c.Context(), consent)
|
||||
}
|
||||
|
||||
if h.AuditRepo != nil {
|
||||
detailsMap := map[string]interface{}{
|
||||
"client_id": consentRequest.Client.ClientID,
|
||||
"scopes": consentRequest.RequestedScope,
|
||||
"client_name": consentRequest.Client.ClientName,
|
||||
}
|
||||
currentSessionID := h.resolveCurrentSessionID(c)
|
||||
if currentSessionID != "" {
|
||||
detailsMap["session_id"] = currentSessionID
|
||||
detailsMap["approved_session_id"] = currentSessionID
|
||||
}
|
||||
detailsMap["auto_accepted"] = true
|
||||
detailsBytes, _ := json.Marshal(detailsMap)
|
||||
|
||||
_ = h.AuditRepo.Create(&domain.AuditLog{
|
||||
EventID: GenerateSecureToken(16),
|
||||
Timestamp: time.Now(),
|
||||
UserID: consentRequest.Subject,
|
||||
TenantID: tenantID, // Uses the tenantID extracted earlier
|
||||
SessionID: currentSessionID,
|
||||
EventType: "consent.granted",
|
||||
Status: "success",
|
||||
IPAddress: c.IP(),
|
||||
UserAgent: string(c.Request().Header.UserAgent()),
|
||||
Details: string(detailsBytes),
|
||||
})
|
||||
}
|
||||
|
||||
slog.Info("Consent skipped and auto-accepted", "subject", consentRequest.Subject, "client", consentRequest.Client.ClientID)
|
||||
return c.JSON(acceptResp)
|
||||
}
|
||||
@@ -5205,11 +5302,24 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
c.Locals("login_id", loginID)
|
||||
}
|
||||
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 := withOidcSessionMetadata(
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope),
|
||||
buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope, tenantID),
|
||||
currentSessionID,
|
||||
)
|
||||
|
||||
// [Debug] 실제 생성된 클레임 출력 (요청사항 확인용)
|
||||
if debugClaimsJSON, err := json.MarshalIndent(sessionClaims, "", " "); err == nil {
|
||||
slog.Info("=== [ACTUAL DATA] GENERATED OIDC CLAIMS ===", "claims", string(debugClaimsJSON))
|
||||
}
|
||||
|
||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims)
|
||||
if err != nil {
|
||||
slog.Error("failed to accept hydra consent request", "error", err)
|
||||
@@ -5245,6 +5355,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
|
||||
SessionID: currentSessionID,
|
||||
EventType: "consent.granted",
|
||||
Status: "success",
|
||||
|
||||
Reference in New Issue
Block a user