From efab2a729159c40c803d171a7a1e3c087d69e0f3 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 17 Jun 2026 11:50:34 +0900 Subject: [PATCH] =?UTF-8?q?profile=20=ED=81=B4=EB=A0=88=EC=9E=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 93 ++++++++++++++----- .../auth_handler_dynamic_claims_test.go | 39 ++++++-- docs/rp-iam-integration-guide.md | 12 ++- 3 files changed, 111 insertions(+), 33 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index a54e91a6..f0294cd0 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1040,7 +1040,7 @@ func normalizePhoneForLoginID(phone string) string { return domain.NormalizePhoneNumber(phone) } -func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID string) map[string]any { +func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID string, profileStatus string) map[string]any { claims := map[string]any{} if traits == nil { return claims @@ -1089,31 +1089,27 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID if _, ok := scopeSet["profile"]; ok { profile := map[string]any{} - names := map[string]any{} - for _, key := range []string{ - "name", - "displayname", - "preferred_username", - "given_name", - "family_name", - "middle_name", - "nickname", - } { - if value := getString(key); value != "" { - names[key] = value - } + displayProfileName := displayName + if displayProfileName != "" { + profile["name"] = displayProfileName } - if len(names) > 0 { - profile["names"] = names + if primaryEmail != "" { + profile["email"] = primaryEmail } - - emails := collectEmailList(traits, primaryEmail) - if len(emails) > 0 { - profile["emails"] = emails + if secondaryEmails := collectStringValues(traits, "sub_email", "secondary_emails", "additional_emails", "aliasEmails", "worksmobileAliasEmails"); len(secondaryEmails) > 0 { + profile["secondary_emails"] = secondaryEmails } if phone := getString("phone_number"); phone != "" { profile["phones"] = []string{phone} } + if employeeID := getString("employee_id"); employeeID != "" { + profile["employee_id"] = employeeID + } + if trimmedStatus := strings.TrimSpace(profileStatus); trimmedStatus != "" { + if normalizedStatus := strings.TrimSpace(domain.NormalizeUserStatus(trimmedStatus)); normalizedStatus != "" { + profile["status"] = normalizedStatus + } + } if len(profile) > 0 { claims["profile"] = profile } @@ -1218,13 +1214,63 @@ func withRefreshTokenExpiryClaim(claims map[string]any, issuedAt time.Time) map[ return claims } -func composeOIDCSessionClaims(client domain.HydraClient, traits map[string]any, scopes []string, tenantID string, sessionID string) map[string]any { - claims := buildOidcClaimsFromTraits(traits, scopes, tenantID) +func composeOIDCSessionClaims(client domain.HydraClient, traits map[string]any, scopes []string, tenantID string, sessionID string, profileStatus string) map[string]any { + claims := buildOidcClaimsFromTraits(traits, scopes, tenantID, profileStatus) claims = applyConfiguredIDTokenClaims(claims, client.Metadata) claims = withRefreshTokenExpiryClaim(claims, time.Now()) return withOidcSessionMetadata(claims, sessionID) } +func collectStringValues(traits map[string]any, keys ...string) []string { + values := make([]string, 0) + seen := make(map[string]struct{}) + add := func(raw string) { + value := strings.TrimSpace(raw) + if value == "" { + return + } + if _, ok := seen[value]; ok { + return + } + seen[value] = struct{}{} + values = append(values, value) + } + for _, key := range keys { + raw, ok := traits[key] + if !ok || raw == nil { + continue + } + switch value := raw.(type) { + case string: + add(value) + case []string: + for _, item := range value { + add(item) + } + case []any: + for _, item := range value { + add(fmt.Sprint(item)) + } + } + } + return values +} + +func (h *AuthHandler) resolveProfileStatus(ctx context.Context, subject string) string { + if h == nil || h.UserRepo == nil { + return "" + } + subject = strings.TrimSpace(subject) + if subject == "" { + return "" + } + user, err := h.UserRepo.FindByID(ctx, subject) + if err != nil || user == nil { + return "" + } + return domain.NormalizeUserStatus(user.Status) +} + func applyGlobalCustomClaims(baseClaims map[string]any, traits map[string]any) map[string]any { if baseClaims == nil { baseClaims = map[string]any{} @@ -6298,6 +6344,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { consentRequest.RequestedScope, representativeTenantIDFromTraits(identity.Traits), currentSessionID, + h.resolveProfileStatus(c.Context(), consentRequest.Subject), ) sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope) sessionClaims = h.withRPUserMetadataClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject) @@ -6336,6 +6383,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { consentRequest.RequestedScope, representativeTenantIDFromTraits(identity.Traits), currentSessionID, + h.resolveProfileStatus(c.Context(), consentRequest.Subject), ) sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope) sessionClaims = h.withRPUserMetadataClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject) @@ -6527,6 +6575,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error { consentRequest.RequestedScope, representativeTenantIDFromTraits(identity.Traits), currentSessionID, + h.resolveProfileStatus(c.Context(), consentRequest.Subject), ) sessionClaims = h.withHanmacFamilyTenantClaims(c.Context(), sessionClaims, identity.Traits, consentRequest.RequestedScope) sessionClaims = h.withRPUserMetadataClaims(c.Context(), sessionClaims, consentRequest.Client, consentRequest.Subject) diff --git a/backend/internal/handler/auth_handler_dynamic_claims_test.go b/backend/internal/handler/auth_handler_dynamic_claims_test.go index 900ab75e..2ee5a025 100644 --- a/backend/internal/handler/auth_handler_dynamic_claims_test.go +++ b/backend/internal/handler/auth_handler_dynamic_claims_test.go @@ -73,6 +73,8 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { "email": "user@baron.com", "name": "홍길동", "phone_number": "+821012345678", + "employee_id": "EMP-001", + "sub_email": []any{"alias1@baron.com", "alias2@baron.com"}, "tenant_id": "primary-tenant-999", // Added primary tenant "tenant-1": map[string]any{ "department": "개발팀", @@ -86,13 +88,18 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { scopes := []string{"openid", "profile"} t.Run("No tenantID", func(t *testing.T) { - claims := buildOidcClaimsFromTraits(traits, scopes, "") + claims := buildOidcClaimsFromTraits(traits, scopes, "", "leave_of_absence") assert.Equal(t, "user@baron.com", claims["email"]) assert.Equal(t, "홍길동", claims["name"]) assert.Equal(t, "primary-tenant-999", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) profile := claims["profile"].(map[string]any) + assert.Equal(t, "홍길동", profile["name"]) + assert.Equal(t, "user@baron.com", profile["email"]) + assert.Equal(t, "EMP-001", profile["employee_id"]) + assert.Equal(t, []string{"alias1@baron.com", "alias2@baron.com"}, profile["secondary_emails"]) + assert.Equal(t, "temporary_leave", profile["status"]) assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) @@ -102,13 +109,18 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { }) t.Run("With tenant-1", func(t *testing.T) { - claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-1") + claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-1", "leave_of_absence") assert.Equal(t, "user@baron.com", claims["email"]) assert.Equal(t, "홍길동", claims["name"]) assert.Equal(t, "tenant-1", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) profile := claims["profile"].(map[string]any) + assert.Equal(t, "홍길동", profile["name"]) + assert.Equal(t, "user@baron.com", profile["email"]) + assert.Equal(t, "EMP-001", profile["employee_id"]) + assert.Equal(t, []string{"alias1@baron.com", "alias2@baron.com"}, profile["secondary_emails"]) + assert.Equal(t, "temporary_leave", profile["status"]) assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) @@ -118,13 +130,18 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { }) t.Run("With tenant-2", func(t *testing.T) { - claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-2") + claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-2", "leave_of_absence") assert.Equal(t, "user@baron.com", claims["email"]) assert.Equal(t, "홍길동", claims["name"]) assert.Equal(t, "tenant-2", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) profile := claims["profile"].(map[string]any) + assert.Equal(t, "홍길동", profile["name"]) + assert.Equal(t, "user@baron.com", profile["email"]) + assert.Equal(t, "EMP-001", profile["employee_id"]) + assert.Equal(t, []string{"alias1@baron.com", "alias2@baron.com"}, profile["secondary_emails"]) + assert.Equal(t, "temporary_leave", profile["status"]) assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) @@ -132,13 +149,18 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { }) t.Run("With non-existent tenant", func(t *testing.T) { - claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-3") + claims := buildOidcClaimsFromTraits(traits, scopes, "tenant-3", "leave_of_absence") assert.Equal(t, "user@baron.com", claims["email"]) assert.Equal(t, "홍길동", claims["name"]) assert.Equal(t, "tenant-3", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) profile := claims["profile"].(map[string]any) + assert.Equal(t, "홍길동", profile["name"]) + assert.Equal(t, "user@baron.com", profile["email"]) + assert.Equal(t, "EMP-001", profile["employee_id"]) + assert.Equal(t, []string{"alias1@baron.com", "alias2@baron.com"}, profile["secondary_emails"]) + assert.Equal(t, "temporary_leave", profile["status"]) assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) @@ -147,11 +169,16 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { }) t.Run("Tenants scope includes detailed tenant metadata", func(t *testing.T) { - claims := buildOidcClaimsFromTraits(traits, []string{"openid", "profile", "tenants"}, "tenant-1") + claims := buildOidcClaimsFromTraits(traits, []string{"openid", "profile", "tenants"}, "tenant-1", "leave_of_absence") assert.Equal(t, "tenant-1", claims["tenant_id"]) assert.Equal(t, "개발팀", claims["department"]) assert.Equal(t, "선임", claims["grade"]) profile := claims["profile"].(map[string]any) + assert.Equal(t, "홍길동", profile["name"]) + assert.Equal(t, "user@baron.com", profile["email"]) + assert.Equal(t, "EMP-001", profile["employee_id"]) + assert.Equal(t, []string{"alias1@baron.com", "alias2@baron.com"}, profile["secondary_emails"]) + assert.Equal(t, "temporary_leave", profile["status"]) assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.NotNil(t, claims["tenants"]) assert.Contains(t, claims["joined_tenants"], "tenant-1") @@ -856,7 +883,7 @@ func TestBuildOidcClaimsFromTraits_IncludesGlobalCustomClaims(t *testing.T) { "writePermission": "admin_only", }, }, - }, []string{"openid", "profile", "email"}, "") + }, []string{"openid", "profile", "email"}, "", "") assert.Equal(t, "2026-06-09", claims["contract_date"]) assert.Equal(t, "2026-06-09T09:30:00+09:00", claims["approved_at"]) diff --git a/docs/rp-iam-integration-guide.md b/docs/rp-iam-integration-guide.md index 5574aaa9..a9c8a8ae 100644 --- a/docs/rp-iam-integration-guide.md +++ b/docs/rp-iam-integration-guide.md @@ -238,15 +238,17 @@ Baron은 기본적으로 대표소속 tenant와 전체 소속 tenant 목록을 } }, "profile": { - "emails": [ - "hanmac-user@example.com" + "email": "hanmac-user@example.com", + "secondary_emails": [ + "alias1@hanmaceng.co.kr", + "alias2@hanmaceng.co.kr" ], "phones": [ "+821012345678" ], - "names": { - "name": "한맥 사용자" - } + "name": "한맥 사용자", + "employee_id": "EMP-001", + "status": "temporary_leave" } } ```