forked from baron/baron-sso
Merge pull request 'feature/custom-claim-ui' (#1118) from feature/custom-claim-ui into dev
Reviewed-on: baron/baron-sso#1118
This commit is contained in:
@@ -8430,7 +8430,7 @@ func buildHydraAuthorizationURL(clientID string, scopes []string, redirectURIs [
|
|||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
for _, scope := range append([]string{"openid"}, scopes...) {
|
for _, scope := range append([]string{"openid"}, scopes...) {
|
||||||
scope = strings.TrimSpace(scope)
|
scope = strings.TrimSpace(scope)
|
||||||
if scope == "" || isRefreshTokenScopeAlias(scope) {
|
if scope == "" || isLegacyRefreshTokenScopeAlias(scope) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := seen[scope]; ok {
|
if _, ok := seen[scope]; ok {
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string {
|
|||||||
|
|
||||||
appendIfPresent := func(scope string) {
|
appendIfPresent := func(scope string) {
|
||||||
scope = strings.TrimSpace(scope)
|
scope = strings.TrimSpace(scope)
|
||||||
if scope == "" || isRefreshTokenScopeAlias(scope) {
|
if scope == "" || isLegacyRefreshTokenScopeAlias(scope) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := seen[scope]; ok {
|
if _, ok := seen[scope]; ok {
|
||||||
@@ -485,7 +485,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string {
|
|||||||
|
|
||||||
for _, scope := range combined {
|
for _, scope := range combined {
|
||||||
scope = strings.TrimSpace(scope)
|
scope = strings.TrimSpace(scope)
|
||||||
if scope == "" || isRefreshTokenScopeAlias(scope) {
|
if scope == "" || isLegacyRefreshTokenScopeAlias(scope) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := seen[scope]; ok {
|
if _, ok := seen[scope]; ok {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -153,7 +154,7 @@ func TestMergeRequestedScopesWithClientRequirements_StripsRefreshTokenScopeAlias
|
|||||||
[]string{"openid", "offline", "profile", "offline_access"},
|
[]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) {
|
func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) {
|
||||||
@@ -166,10 +167,11 @@ func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T)
|
|||||||
parsed, err := url.Parse(urlString)
|
parsed, err := url.Parse(urlString)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
scopes := parsed.Query().Get("scope")
|
scopes := parsed.Query().Get("scope")
|
||||||
|
scopeItems := strings.Fields(scopes)
|
||||||
|
|
||||||
assert.Equal(t, "openid profile email", scopes)
|
assert.Equal(t, "openid profile offline_access email", scopes)
|
||||||
assert.NotContains(t, scopes, "offline")
|
assert.NotContains(t, scopeItems, "offline")
|
||||||
assert.NotContains(t, scopes, "offline_access")
|
assert.Contains(t, scopeItems, "offline_access")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) {
|
func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) {
|
||||||
|
|||||||
@@ -3828,7 +3828,7 @@ func requestIncludesInlineHeadlessJWKS(req clientUpsertRequest) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultClientScopes() []string {
|
func defaultClientScopes() []string {
|
||||||
return []string{"openid", "profile", "email"}
|
return []string{"openid", "profile", "email", "offline_access"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultGrantTypes() []string {
|
func defaultGrantTypes() []string {
|
||||||
@@ -3848,7 +3848,7 @@ func normalizeClientScopes(scopes []string) []string {
|
|||||||
seen := make(map[string]struct{}, len(scopes))
|
seen := make(map[string]struct{}, len(scopes))
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
scope = strings.TrimSpace(scope)
|
scope = strings.TrimSpace(scope)
|
||||||
if scope == "" || isRefreshTokenScopeAlias(scope) {
|
if scope == "" || isLegacyRefreshTokenScopeAlias(scope) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := seen[scope]; ok {
|
if _, ok := seen[scope]; ok {
|
||||||
@@ -3860,9 +3860,9 @@ func normalizeClientScopes(scopes []string) []string {
|
|||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
func isRefreshTokenScopeAlias(scope string) bool {
|
func isLegacyRefreshTokenScopeAlias(scope string) bool {
|
||||||
switch strings.ToLower(strings.TrimSpace(scope)) {
|
switch strings.ToLower(strings.TrimSpace(scope)) {
|
||||||
case "offline", "offline_access":
|
case "offline":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -2229,9 +2229,9 @@ func TestCreateClient_StripsOfflineScopesAndKeepsRefreshTokenGrant(t *testing.T)
|
|||||||
|
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
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")
|
||||||
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")
|
assert.Contains(t, captured.GrantTypes, "refresh_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2296,9 +2296,9 @@ func TestUpdateClient_StripsStoredOfflineScopesAndKeepsRefreshTokenGrant(t *test
|
|||||||
|
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
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")
|
||||||
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")
|
assert.Contains(t, captured.GrantTypes, "refresh_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1060,7 +1060,14 @@ function ClientConsentsPage() {
|
|||||||
aria-label={`${row.key} ${row.valueType}`}
|
aria-label={`${row.key} ${row.valueType}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex gap-2",
|
||||||
|
row.valueType === "date" || row.valueType === "datetime"
|
||||||
|
? "items-center"
|
||||||
|
: "flex-col",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
type={rpClaimInputType(row.valueType)}
|
type={rpClaimInputType(row.valueType)}
|
||||||
inputMode={rpClaimInputMode(row.valueType)}
|
inputMode={rpClaimInputMode(row.valueType)}
|
||||||
@@ -1087,7 +1094,7 @@ function ClientConsentsPage() {
|
|||||||
timeZone: event.target.value,
|
timeZone: event.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="h-10 rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
className="h-10 min-w-[160px] rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
aria-label={`${row.key} timezone`}
|
aria-label={`${row.key} timezone`}
|
||||||
>
|
>
|
||||||
{timeZoneOptions.map((timeZone) => (
|
{timeZoneOptions.map((timeZone) => (
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ describe("ClientGeneralPage RP claims", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows supported scopes and custom claims without integrated offline_access from the add scope button", async () => {
|
it("shows supported scopes including offline_access and custom claims from the add scope button", async () => {
|
||||||
const { container } = await renderPage();
|
const { container } = await renderPage();
|
||||||
|
|
||||||
const addScopeButton = Array.from(
|
const addScopeButton = Array.from(
|
||||||
@@ -422,7 +422,7 @@ describe("ClientGeneralPage RP claims", () => {
|
|||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).not.toContain("offline_access");
|
expect(container.textContent).toContain("offline_access");
|
||||||
expect(container.textContent).toContain("old_claim");
|
expect(container.textContent).toContain("old_claim");
|
||||||
|
|
||||||
const customClaimButton = Array.from(
|
const customClaimButton = Array.from(
|
||||||
|
|||||||
@@ -659,6 +659,15 @@ function ClientGeneralPage() {
|
|||||||
description: t("msg.dev.clients.scopes.email", "이메일 주소 접근"),
|
description: t("msg.dev.clients.scopes.email", "이메일 주소 접근"),
|
||||||
mandatory: false,
|
mandatory: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
name: "offline_access",
|
||||||
|
description: t(
|
||||||
|
"msg.dev.clients.scopes.offline_access",
|
||||||
|
"refresh token 발급 요청",
|
||||||
|
),
|
||||||
|
mandatory: false,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);
|
const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);
|
||||||
const browserTimeZone = useMemo(() => getBrowserTimeZone(), []);
|
const browserTimeZone = useMemo(() => getBrowserTimeZone(), []);
|
||||||
@@ -759,6 +768,15 @@ function ClientGeneralPage() {
|
|||||||
description: tenantScopeDescription,
|
description: tenantScopeDescription,
|
||||||
source: "standard",
|
source: "standard",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "standard-offline-access",
|
||||||
|
name: "offline_access",
|
||||||
|
description: t(
|
||||||
|
"msg.dev.clients.scopes.offline_access",
|
||||||
|
"refresh token 발급 요청",
|
||||||
|
),
|
||||||
|
source: "standard",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[tenantScopeDescription],
|
[tenantScopeDescription],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ test.describe("DevFront RP claim cache", () => {
|
|||||||
await expect(claimKeyInput).toHaveValue("new_claim");
|
await expect(claimKeyInput).toHaveValue("new_claim");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("adds supported scopes and custom claim keys from the scope picker without offline_access", async ({
|
test("adds supported scopes and custom claim keys from the scope picker including offline_access", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -142,9 +142,9 @@ test.describe("DevFront RP claim cache", () => {
|
|||||||
.getByRole("button", { name: /스코프 추가|Scope 추가|Add Scope/i })
|
.getByRole("button", { name: /스코프 추가|Scope 추가|Add Scope/i })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(page.getByText("offline_access", { exact: true })).toHaveCount(
|
await expect(
|
||||||
0,
|
page.getByText("offline_access", { exact: true }),
|
||||||
);
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("button", { name: /employee_code/ }),
|
page.getByRole("button", { name: /employee_code/ }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user