diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index e92041d3..185a6c68 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -8430,7 +8430,7 @@ func buildHydraAuthorizationURL(clientID string, scopes []string, redirectURIs [ seen := map[string]struct{}{} for _, scope := range append([]string{"openid"}, scopes...) { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } if _, ok := seen[scope]; ok { diff --git a/backend/internal/handler/client_tenant_access.go b/backend/internal/handler/client_tenant_access.go index 670980b4..a5f9bbe3 100644 --- a/backend/internal/handler/client_tenant_access.go +++ b/backend/internal/handler/client_tenant_access.go @@ -464,7 +464,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { appendIfPresent := func(scope string) { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { return } if _, ok := seen[scope]; ok { @@ -485,7 +485,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { for _, scope := range combined { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } if _, ok := seen[scope]; ok { diff --git a/backend/internal/handler/client_tenant_access_test.go b/backend/internal/handler/client_tenant_access_test.go index 29caeeb9..661cd631 100644 --- a/backend/internal/handler/client_tenant_access_test.go +++ b/backend/internal/handler/client_tenant_access_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" "github.com/gofiber/fiber/v2" @@ -153,7 +154,7 @@ func TestMergeRequestedScopesWithClientRequirements_StripsRefreshTokenScopeAlias []string{"openid", "offline", "profile", "offline_access"}, ) - assert.Equal(t, []string{"openid", "tenant", "profile", "email"}, merged) + assert.Equal(t, []string{"openid", "tenant", "profile", "offline_access", "email"}, merged) } func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) { @@ -166,10 +167,11 @@ func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) parsed, err := url.Parse(urlString) assert.NoError(t, err) scopes := parsed.Query().Get("scope") + scopeItems := strings.Fields(scopes) - assert.Equal(t, "openid profile email", scopes) - assert.NotContains(t, scopes, "offline") - assert.NotContains(t, scopes, "offline_access") + assert.Equal(t, "openid profile offline_access email", scopes) + assert.NotContains(t, scopeItems, "offline") + assert.Contains(t, scopeItems, "offline_access") } func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) { diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index 014031f5..eacfdd83 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -3828,7 +3828,7 @@ func requestIncludesInlineHeadlessJWKS(req clientUpsertRequest) bool { } func defaultClientScopes() []string { - return []string{"openid", "profile", "email"} + return []string{"openid", "profile", "email", "offline_access"} } func defaultGrantTypes() []string { @@ -3848,7 +3848,7 @@ func normalizeClientScopes(scopes []string) []string { seen := make(map[string]struct{}, len(scopes)) for _, scope := range scopes { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } if _, ok := seen[scope]; ok { @@ -3860,9 +3860,9 @@ func normalizeClientScopes(scopes []string) []string { return normalized } -func isRefreshTokenScopeAlias(scope string) bool { +func isLegacyRefreshTokenScopeAlias(scope string) bool { switch strings.ToLower(strings.TrimSpace(scope)) { - case "offline", "offline_access": + case "offline": return true default: return false diff --git a/backend/internal/handler/dev_handler_test.go b/backend/internal/handler/dev_handler_test.go index d750e9fd..0ababf7b 100644 --- a/backend/internal/handler/dev_handler_test.go +++ b/backend/internal/handler/dev_handler_test.go @@ -2229,9 +2229,9 @@ func TestCreateClient_StripsOfflineScopesAndKeepsRefreshTokenGrant(t *testing.T) resp, _ := app.Test(req, -1) assert.Equal(t, http.StatusCreated, resp.StatusCode) - assert.Equal(t, "openid profile email", captured.Scope) + assert.Equal(t, "openid profile offline_access email", captured.Scope) assert.NotContains(t, strings.Fields(captured.Scope), "offline") - assert.NotContains(t, strings.Fields(captured.Scope), "offline_access") + assert.Contains(t, strings.Fields(captured.Scope), "offline_access") assert.Contains(t, captured.GrantTypes, "refresh_token") } @@ -2296,9 +2296,9 @@ func TestUpdateClient_StripsStoredOfflineScopesAndKeepsRefreshTokenGrant(t *test resp, _ := app.Test(req, -1) assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, "openid profile email", captured.Scope) + assert.Equal(t, "openid profile offline_access email", captured.Scope) assert.NotContains(t, strings.Fields(captured.Scope), "offline") - assert.NotContains(t, strings.Fields(captured.Scope), "offline_access") + assert.Contains(t, strings.Fields(captured.Scope), "offline_access") assert.Contains(t, captured.GrantTypes, "refresh_token") } diff --git a/devfront/src/features/clients/ClientConsentsPage.test.tsx b/devfront/src/features/clients/ClientConsentsPage.test.tsx index c8d9cede..7a76d587 100644 --- a/devfront/src/features/clients/ClientConsentsPage.test.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.test.tsx @@ -201,4 +201,85 @@ describe("ClientConsentsPage RP custom claims", () => { }), ); }); + + it("keeps date claim inputs and timezone selectors on the same row", async () => { + fetchClientMock.mockResolvedValue({ + ...clientDetail, + client: { + ...clientDetail.client, + metadata: { + id_token_claims: [ + { + namespace: "rp_claims", + key: "contract_date", + value: "", + valueType: "date", + readPermission: "admin_only", + writePermission: "admin_only", + }, + ], + }, + }, + }); + fetchConsentsMock.mockResolvedValue({ + items: [ + { + subject: "user-1", + userName: "Consent User", + clientId: "client-a", + clientName: "Claims App", + grantedScopes: ["openid", "profile"], + authenticatedAt: "2026-06-11T09:00:00Z", + createdAt: "2026-06-10T09:00:00Z", + status: "active", + tenantId: "tenant-1", + tenantName: "Hanmac", + rpMetadata: { + contract_date: 1781017200, + contract_date_permissions: { + readPermission: "admin_only", + writePermission: "admin_only", + }, + }, + }, + ], + }); + fetchRPUserMetadataMock.mockResolvedValue({ + clientId: "client-a", + userId: "user-1", + metadata: { + contract_date: 1781017200, + contract_date_permissions: { + readPermission: "admin_only", + writePermission: "admin_only", + }, + }, + }); + + const { container } = await renderPage(); + + const editButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("사용자 Claim 설정") || + button.textContent?.includes("User Claim Settings"), + ); + + await act(async () => { + editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + const dateInput = container.querySelector( + 'input[aria-label="contract_date date"]', + ); + const timeZoneSelect = container.querySelector( + 'select[aria-label="contract_date timezone"]', + ); + + expect(dateInput).not.toBeNull(); + expect(timeZoneSelect).not.toBeNull(); + expect(dateInput?.parentElement).toBe(timeZoneSelect?.parentElement); + expect(dateInput?.parentElement?.className).toContain("items-center"); + expect(dateInput?.parentElement?.className).not.toContain("flex-col"); + }); }); diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index 1e6f1fa9..9ccdfcf8 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -1060,7 +1060,14 @@ function ClientConsentsPage() { aria-label={`${row.key} ${row.valueType}`} /> ) : ( -