forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
@@ -157,6 +157,8 @@ function ClientGeneralPage() {
|
||||
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
|
||||
const [tenantSearch, setTenantSearch] = useState("");
|
||||
const [isTenantSearchOpen, setIsTenantSearchOpen] = useState(false);
|
||||
const [autoLoginSupported, setAutoLoginSupported] = useState(false);
|
||||
const [autoLoginUrl, setAutoLoginUrl] = useState("");
|
||||
|
||||
// Public Key Registration States
|
||||
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
|
||||
@@ -203,6 +205,9 @@ function ClientGeneralPage() {
|
||||
if (typeof metadata.description === "string")
|
||||
setDescription(metadata.description);
|
||||
if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url);
|
||||
setAutoLoginSupported(metadata.auto_login_supported === true);
|
||||
if (typeof metadata.auto_login_url === "string")
|
||||
setAutoLoginUrl(metadata.auto_login_url);
|
||||
|
||||
const headlessEnabled = !!metadata.headless_login_enabled;
|
||||
setHeadlessLoginEnabled(headlessEnabled);
|
||||
@@ -287,8 +292,12 @@ function ClientGeneralPage() {
|
||||
const securityProfile: SecurityProfile =
|
||||
clientType === "pkce" ? "pkce" : "private";
|
||||
const trimmedLogoUrl = logoUrl.trim();
|
||||
const trimmedAutoLoginUrl = autoLoginUrl.trim();
|
||||
const hasLogoUrl = trimmedLogoUrl.length > 0;
|
||||
const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl);
|
||||
const hasValidAutoLoginUrl =
|
||||
!autoLoginSupported ||
|
||||
(trimmedAutoLoginUrl.length > 0 && isValidUrl(trimmedAutoLoginUrl));
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasLogoUrl) {
|
||||
@@ -523,6 +532,14 @@ function ClientGeneralPage() {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (autoLoginSupported && !hasValidAutoLoginUrl) {
|
||||
validationErrors.push(
|
||||
t(
|
||||
"msg.dev.clients.general.auto_login.invalid_url",
|
||||
"자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const hasValidationErrors = validationErrors.length > 0;
|
||||
const normalizedTenantSearch = tenantSearch.trim().toLowerCase();
|
||||
@@ -618,6 +635,14 @@ function ClientGeneralPage() {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (autoLoginSupported && !hasValidAutoLoginUrl) {
|
||||
throw new Error(
|
||||
t(
|
||||
"msg.dev.clients.general.auto_login.invalid_url",
|
||||
"자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedScopes = normalizeScopesForTenantAccess(
|
||||
scopes,
|
||||
@@ -648,6 +673,8 @@ function ClientGeneralPage() {
|
||||
metadata: {
|
||||
description,
|
||||
logo_url: trimmedLogoUrl,
|
||||
auto_login_supported: autoLoginSupported,
|
||||
auto_login_url: autoLoginSupported ? trimmedAutoLoginUrl : undefined,
|
||||
structured_scopes: normalizedScopes,
|
||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||
headless_login_enabled: headlessLoginEnabled,
|
||||
@@ -1057,6 +1084,84 @@ function ClientGeneralPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
{t("ui.dev.clients.general.auto_login.title", "자동 로그인")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"msg.dev.clients.general.auto_login.subtitle",
|
||||
"RP가 자체 로그인 시작 URL에서 OIDC 요청을 만들 수 있으면 userfront에서 바로 로그인 진입을 제공합니다.",
|
||||
)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
|
||||
<div className="space-y-0.5 text-right">
|
||||
<p className="text-sm font-semibold">
|
||||
{autoLoginSupported
|
||||
? t("ui.common.enabled", "사용")
|
||||
: t("ui.common.disabled", "사용 안 함")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.auto_login.supported",
|
||||
"자동 로그인 지원",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoLoginSupported}
|
||||
onCheckedChange={setAutoLoginSupported}
|
||||
id="auto-login-supported"
|
||||
aria-label={t(
|
||||
"ui.dev.clients.general.auto_login.supported",
|
||||
"자동 로그인 지원",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="auto-login-url" className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.auto_login.url",
|
||||
"자동 로그인 시작 URL",
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="auto-login-url"
|
||||
value={autoLoginUrl}
|
||||
onChange={(event) => setAutoLoginUrl(event.target.value)}
|
||||
disabled={!autoLoginSupported}
|
||||
aria-invalid={!hasValidAutoLoginUrl}
|
||||
className={!hasValidAutoLoginUrl ? "border-destructive" : ""}
|
||||
placeholder={t(
|
||||
"ui.dev.clients.general.auto_login.url_placeholder",
|
||||
"https://app.example.com/login?auto=1",
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"msg.dev.clients.general.auto_login.help",
|
||||
"이 URL은 RP가 state, nonce, PKCE 값을 직접 생성한 뒤 Baron OIDC로 리다이렉트해야 합니다.",
|
||||
)}
|
||||
</p>
|
||||
{!hasValidAutoLoginUrl ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.auto_login.invalid_url",
|
||||
"자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2. Scopes */}
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
|
||||
@@ -273,6 +273,33 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
).toHaveValue(jwksUri);
|
||||
});
|
||||
|
||||
test("auto login settings are stored in client metadata", async ({ page }) => {
|
||||
const autoLoginUrl = "https://rp.example.com/login?auto=1";
|
||||
const state = {
|
||||
clients: [makeClient("client-auto-login", { name: "Auto Login app" })],
|
||||
consents: [] as Consent[],
|
||||
auditLogsByCursor: undefined,
|
||||
};
|
||||
await installDevApiMock(page, state);
|
||||
|
||||
await page.goto("/clients/client-auto-login/settings");
|
||||
|
||||
await page
|
||||
.getByRole("switch", { name: /자동 로그인 지원|Auto Login/i })
|
||||
.click();
|
||||
await page
|
||||
.getByPlaceholder(/https:\/\/app\.example\.com\/login\?auto=1/i)
|
||||
.fill(autoLoginUrl);
|
||||
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||
|
||||
await expect
|
||||
.poll(() => state.clients[0]?.metadata?.auto_login_supported)
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => state.clients[0]?.metadata?.auto_login_url)
|
||||
.toBe(autoLoginUrl);
|
||||
});
|
||||
|
||||
test("pkce headless login blocks save when parsed jwks algorithm is unsupported", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user