+
-
-
+
-
+
+
+
)}
diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx
index a372fef0..1fa793c6 100644
--- a/devfront/src/features/clients/ClientDetailsPage.tsx
+++ b/devfront/src/features/clients/ClientDetailsPage.tsx
@@ -1,6 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
-import { Eye, EyeOff, Link2, RefreshCw, Save, Shield } from "lucide-react";
+import {
+ ArrowLeft,
+ Eye,
+ EyeOff,
+ Link2,
+ RefreshCw,
+ Save,
+ Shield,
+} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { Badge } from "../../components/ui/badge";
@@ -183,29 +191,42 @@ function ClientDetailsPage() {
return (
-
-
- {t("ui.dev.clients.details.breadcrumb.section", "Apps")}
+
+
-
-
- {data.client.name || data.client.id}
-
-
- {t(
- "msg.dev.clients.details.subtitle",
- "OIDC 자격 증명과 엔드포인트를 관리합니다.",
- )}
-
+
+
+
+
+ {data.client.name || data.client.id}
+
+
+ {t(
+ "msg.dev.clients.details.subtitle",
+ "Manage OIDC credentials and endpoints.",
+ )}
+
+
{data.client.status === "active"
@@ -241,7 +262,7 @@ function ClientDetailsPage() {
{t(
"ui.dev.clients.details.credentials.title",
- "클라이언트 자격 증명",
+ "Client Credentials",
)}
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx
index 349bba58..91bcd6cc 100644
--- a/devfront/src/features/clients/ClientGeneralPage.tsx
+++ b/devfront/src/features/clients/ClientGeneralPage.tsx
@@ -1,6 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
-import { Plus, Save, Shield, Sparkles, Trash2, Upload } from "lucide-react";
+import {
+ ArrowLeft,
+ Plus,
+ Save,
+ Shield,
+ Sparkles,
+ Trash2,
+ Upload,
+} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Badge } from "../../components/ui/badge";
@@ -261,22 +269,41 @@ function ClientGeneralPage() {
-
-
- {t("ui.dev.clients.general.breadcrumb.section", "Applications")}
+
+
+
+
+ {isCreate
+ ? t("ui.dev.clients.general.title_create", "Create Client")
+ : t("ui.dev.clients.general.title_edit", "Client Settings")}
+
-
- {isCreate
- ? t("ui.dev.clients.general.title_create", "Create Client")
- : t("ui.dev.clients.general.title_edit", "Client Settings")}
-
{!isCreate && (
{status === "active"
@@ -292,7 +319,7 @@ function ClientGeneralPage() {
to={`/clients/${clientId}`}
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
>
- {t("ui.dev.clients.details.tab.connection", "Connection")}
+ {t("ui.dev.clients.details.tab.connection", "Federation")}
{client.status === "active"
diff --git a/devfront/src/features/clients/routes/ClientFederationPage.tsx b/devfront/src/features/clients/routes/ClientFederationPage.tsx
index c61c20be..6700e7fe 100644
--- a/devfront/src/features/clients/routes/ClientFederationPage.tsx
+++ b/devfront/src/features/clients/routes/ClientFederationPage.tsx
@@ -267,7 +267,7 @@ export function ClientFederationPage() {
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml
index 45da9846..dcceb9bf 100644
--- a/devfront/src/locales/en.toml
+++ b/devfront/src/locales/en.toml
@@ -227,25 +227,25 @@ subtitle = "Subtitle"
[msg.dev.clients.details]
copy_client_id = "Client ID copied."
-copy_client_secret = "Copy Client Secret"
+copy_client_secret = "Client Secret copied."
copy_endpoint = "{{label}} copied."
load_error = "Error loading client: {{error}}"
loading = "Loading client..."
missing_id = "Client ID is required."
redirect_saved = "Redirect URIs saved."
-rotate_confirm = "Rotate Confirm"
-rotate_error = "Rotate Error"
-save_error = "Save Error"
-secret_rotated = "Secret Rotated"
+rotate_confirm = "Warning: Rotating the Client Secret will invalidate the existing secret immediately.\nConnected applications may experience downtime. Do you want to proceed?"
+rotate_error = "Failed to rotate secret: {{error}}"
+save_error = "Failed to save: {{error}}"
+secret_rotated = "Client Secret has been rotated."
secret_unavailable = "SECRET_NOT_AVAILABLE"
-subtitle = "Subtitle"
+subtitle = "Manage OIDC credentials and endpoints."
[msg.dev.clients.details.redirect]
-description = "Description"
+description = "A list of allowed URLs to redirect users to after successful authentication. You can enter multiple URLs separated by commas."
[msg.dev.clients.details.security]
-footer = "Footer"
-note = "Note"
+footer = "We recommend verifying admin session TTL, applying rate limits, and setting up notifications for secret rotation."
+note = "Keep endpoints read-only and ensure that secret rotation and copying are tracked in audit logs."
[msg.dev.clients.general]
load_error = "Error loading client: {{error}}"
@@ -301,8 +301,8 @@ dev_scope = "Dev Scope"
hydra_health = "Hydra Health"
[msg.dev.sidebar]
-notice = "Notice"
-notice_detail = "Notice Detail"
+notice = "Developer Console"
+notice_detail = "Register and manage client applications."
[msg.info]
saved_success = "Saved successfully."
@@ -978,6 +978,13 @@ active_grants = "Active Grants"
avg_scopes = "Avg. Scopes per User"
total_scopes = "Total Scopes Issued"
+[ui.dev.clients.stats]
+total = "Total Applications"
+active_sessions = "Active Sessions"
+auth_failures = "Auth Failures (24h)"
+realtime = "Realtime"
+stable = "Stable"
+
[ui.dev.clients.consents.table]
action = "Action"
first_granted = "First Granted"
@@ -990,24 +997,24 @@ user = "User"
[ui.dev.clients.details]
[ui.dev.clients.details.breadcrumb]
-current = "Current"
-section = "Applications"
+current = "App Details"
+section = "Connected Applications"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
client_secret = "Client Secret"
-title = "Title"
+title = "Client Credentials"
[ui.dev.clients.details.endpoints]
read_only = "Read Only"
-title = "Title"
+title = "OIDC Endpoints"
[ui.dev.clients.details.redirect]
callback_label = "Callback Label"
label = "Redirect URIs"
placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback"
save = "Save"
-title = "Title"
+title = "Redirection Settings"
[ui.dev.clients.details.secret]
hide = "Hide"
@@ -1015,7 +1022,7 @@ rotate = "Rotate"
show = "Show"
[ui.dev.clients.details.security]
-title = "Title"
+title = "Security Note"
[ui.dev.clients.details.tab]
connection = "Federation"
@@ -1144,6 +1151,7 @@ unknown = "Unknown"
expired = "Session expired"
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
remaining = "Expires in: {{minutes}}m {{seconds}}s"
+refresh = "Refresh session expiry"
[ui.userfront]
app_title = "Baron SW Portal"
diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml
index d832d1e4..69bc9dae 100644
--- a/devfront/src/locales/ko.toml
+++ b/devfront/src/locales/ko.toml
@@ -926,10 +926,10 @@ admin = "Admin"
user = "User"
[ui.common.status]
-active = "Active"
-blocked = "Blocked"
+active = "활성"
+blocked = "차단됨"
failure = "실패"
-inactive = "Inactive"
+inactive = "비활성"
ok = "정상"
pending = "준비 중"
success = "성공"
@@ -979,6 +979,13 @@ active_grants = "활성 권한"
avg_scopes = "사용자당 평균 권한 수"
total_scopes = "전체 부여된 권한 수"
+[ui.dev.clients.stats]
+total = "총 애플리케이션"
+active_sessions = "활성 세션"
+auth_failures = "인증 실패 (24h)"
+realtime = "실시간"
+stable = "안정"
+
[ui.dev.clients.consents.table]
action = "작업"
first_granted = "최초 동의"
@@ -1020,14 +1027,14 @@ title = "보안 메모"
[ui.dev.clients.details.tab]
connection = "연동 설정"
-consents = "Consent & Users"
-settings = "Settings"
+consents = "동의 및 사용자"
+settings = "설정"
[ui.dev.clients.general]
create = "앱 생성"
display_new = "연동 앱 추가"
-title_create = "Create Client"
-title_edit = "Client Settings"
+title_create = "연동 앱 생성"
+title_edit = "연동 앱 설정"
[ui.dev.clients.federation]
title = "Identity Federation"
@@ -1145,6 +1152,7 @@ unknown = "확인 불가"
expired = "세션 만료"
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음"
+refresh = "세션 만료 시간 갱신"
[ui.userfront]
app_title = "Baron SW 포탈"
diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml
index b1cb4d35..ed9939a1 100644
--- a/devfront/src/locales/template.toml
+++ b/devfront/src/locales/template.toml
@@ -990,6 +990,13 @@ active_grants = ""
avg_scopes = ""
total_scopes = ""
+[ui.dev.clients.stats]
+total = ""
+active_sessions = ""
+auth_failures = ""
+realtime = ""
+stable = ""
+
[ui.dev.clients.consents.table]
action = ""
first_granted = ""
@@ -1156,6 +1163,7 @@ unknown = ""
expired = ""
expiring = ""
remaining = ""
+refresh = ""
[ui.userfront]
app_title = ""
diff --git a/locales/en.toml b/locales/en.toml
index 4ac4562c..ec495d5f 100644
--- a/locales/en.toml
+++ b/locales/en.toml
@@ -359,8 +359,8 @@ dev_scope = "Dev Scope"
hydra_health = "Hydra Health"
[msg.dev.sidebar]
-notice = "Notice"
-notice_detail = "Notice Detail"
+notice = "Developer Console"
+notice_detail = "Register and manage client applications."
[msg.info]
saved_success = "Saved successfully."
@@ -1137,6 +1137,13 @@ active_grants = "Active Grants"
avg_scopes = "Avg. Scopes per User"
total_scopes = "Total Scopes Issued"
+[ui.dev.clients.stats]
+total = "Total Applications"
+active_sessions = "Active Sessions"
+auth_failures = "Auth Failures (24h)"
+realtime = "Realtime"
+stable = "Stable"
+
[ui.dev.clients.consents.table]
action = "Action"
first_granted = "First Granted"
@@ -1149,24 +1156,24 @@ user = "User"
[ui.dev.clients.details]
[ui.dev.clients.details.breadcrumb]
-current = "Current"
-section = "Applications"
+current = "App Details"
+section = "Connected Applications"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
client_secret = "Client Secret"
-title = "Title"
+title = "Client Credentials"
[ui.dev.clients.details.endpoints]
read_only = "Read Only"
-title = "Title"
+title = "OIDC Endpoints"
[ui.dev.clients.details.redirect]
callback_label = "Callback Label"
label = "Redirect URIs"
placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback"
save = "Save"
-title = "Title"
+title = "Redirection Settings"
[ui.dev.clients.details.secret]
hide = "Hide"
@@ -1174,7 +1181,7 @@ rotate = "Rotate"
show = "Show"
[ui.dev.clients.details.security]
-title = "Title"
+title = "Security Note"
[ui.dev.clients.details.tab]
connection = "Federation"
diff --git a/locales/ko.toml b/locales/ko.toml
index d69953e6..59767ceb 100644
--- a/locales/ko.toml
+++ b/locales/ko.toml
@@ -1071,10 +1071,10 @@ admin = "Admin"
user = "User"
[ui.common.status]
-active = "Active"
-blocked = "Blocked"
+active = "활성"
+blocked = "차단됨"
failure = "실패"
-inactive = "Inactive"
+inactive = "비활성"
ok = "정상"
pending = "준비 중"
success = "성공"
@@ -1137,6 +1137,13 @@ active_grants = "Active Grants"
avg_scopes = "Avg. Scopes per User"
total_scopes = "Total Scopes Issued"
+[ui.dev.clients.stats]
+total = "총 애플리케이션"
+active_sessions = "활성 세션"
+auth_failures = "인증 실패 (24h)"
+realtime = "실시간"
+stable = "안정"
+
[ui.dev.clients.consents.table]
action = "Action"
first_granted = "First Granted"
@@ -1178,14 +1185,14 @@ title = "보안 메모"
[ui.dev.clients.details.tab]
connection = "연동 설정"
-consents = "Consent & Users"
-settings = "Settings"
+consents = "동의 및 사용자"
+settings = "설정"
[ui.dev.clients.general]
create = "앱 생성"
display_new = "연동 앱 추가"
-title_create = "Create Client"
-title_edit = "Client Settings"
+title_create = "연동 앱 생성"
+title_edit = "연동 앱 설정"
[ui.dev.clients.federation]
title = "Identity Federation"
diff --git a/locales/template.toml b/locales/template.toml
index ecd149df..9a8fb8ce 100644
--- a/locales/template.toml
+++ b/locales/template.toml
@@ -999,6 +999,13 @@ active_grants = ""
avg_scopes = ""
total_scopes = ""
+[ui.dev.clients.stats]
+total = ""
+active_sessions = ""
+auth_failures = ""
+realtime = ""
+stable = ""
+
[ui.dev.clients.consents.table]
action = ""
first_granted = ""
diff --git a/userfront-e2e/tests/oidc-login-challenge.spec.ts b/userfront-e2e/tests/oidc-login-challenge.spec.ts
new file mode 100644
index 00000000..920a69b4
--- /dev/null
+++ b/userfront-e2e/tests/oidc-login-challenge.spec.ts
@@ -0,0 +1,81 @@
+import { expect, test, type Page, type Route } from '@playwright/test';
+
+async function mockUserfrontApisForRepro(
+ page: Page,
+ options: { sessionStatus: number } = { sessionStatus: 401 },
+): Promise {
+ await page.route('**/api/v1/**', async (route: Route) => {
+ const requestUrl = new URL(route.request().url());
+ const path = requestUrl.pathname;
+
+ if (path.endsWith('/api/v1/user/me')) {
+ await route.fulfill({
+ status: options.sessionStatus,
+ contentType: 'application/json',
+ body: JSON.stringify({ error: 'unauthorized' }),
+ });
+ return;
+ }
+
+ if (path.endsWith('/api/v1/client-log')) {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ ok: true }),
+ });
+ return;
+ }
+
+ // Default mock for other APIs
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({}),
+ });
+ });
+}
+
+test.describe('Issue #345 Reproduction (Log-based Validation)', () => {
+ test('비로그인 상태에서 login_challenge와 함께 signin 진입 시 루프 없이 로그가 정상 출력되어야 한다', async ({ page }) => {
+ const logs: string[] = [];
+ page.on('console', msg => {
+ const text = msg.text();
+ logs.push(text);
+ console.log(`[Browser] ${text}`);
+ });
+
+ const requests: string[] = [];
+ page.on('request', request => {
+ if (request.isNavigationRequest()) {
+ requests.push(request.url());
+ }
+ });
+
+ await mockUserfrontApisForRepro(page, { sessionStatus: 401 });
+
+ const targetUrl = '/ko/signin?login_challenge=repro_challenge_12345';
+ await page.goto(targetUrl);
+
+ // WASM 앱 로딩 및 로직 실행 대기
+ await page.waitForTimeout(7000);
+
+ const currentUrl = page.url();
+ const signinNavigations = requests.filter(url => url.includes('/signin'));
+
+ // [검증 1] URL 유지 확인
+ expect(currentUrl).toContain('login_challenge=repro_challenge_12345');
+
+ // [검증 2] 리다이렉트 루프 발생 여부 확인 (최초 진입 1회만 있어야 함)
+ expect(signinNavigations.length).toBeLessThanOrEqual(1);
+
+ // [검증 3] 핵심 로직 로그 확인 (성공의 결정적 증거)
+ // 이전에는 여기서 Exception이 발생했으나, 이제는 아래 로그가 찍혀야 함
+ const hasSuccessLog = logs.some(log =>
+ log.includes('[Auth] OIDC auto-accept: No active session (status: 401)')
+ );
+
+ expect(hasSuccessLog).toBe(true);
+
+ console.log('✅ 루프가 해결되었으며, 로그 검증을 통해 정상 동작을 확인했습니다.');
+ });
+});
diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml
index ec26fe14..60cccd4d 100644
--- a/userfront/assets/translations/ko.toml
+++ b/userfront/assets/translations/ko.toml
@@ -347,10 +347,10 @@ admin = "Admin"
user = "User"
[ui.common.status]
-active = "Active"
-blocked = "Blocked"
+active = "활성"
+blocked = "차단됨"
failure = "실패"
-inactive = "Inactive"
+inactive = "비활성"
ok = "정상"
pending = "준비 중"
success = "성공"
diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart
index 43f51370..e1af67f8 100644
--- a/userfront/lib/features/auth/presentation/login_screen.dart
+++ b/userfront/lib/features/auth/presentation/login_screen.dart
@@ -161,7 +161,14 @@ class _LoginScreenState extends ConsumerState
final provider = pendingProvider ?? AuthTokenStore.getProvider() ?? 'ory';
try {
- await AuthProxyService.checkCookieSession();
+ final status = await AuthProxyService.getSessionStatus(useCookie: true);
+ if (status != 200) {
+ debugPrint(
+ "[Auth] Cookie session check: No active session (status: $status)",
+ );
+ return;
+ }
+
if (!shouldPromoteCookieSession(
currentToken: AuthTokenStore.getToken(),
loginChallenge: loginChallenge,
@@ -242,11 +249,18 @@ class _LoginScreenState extends ConsumerState
}
try {
- await AuthProxyService.checkCookieSession();
- AuthTokenStore.setCookieMode(
- provider: AuthTokenStore.getProvider() ?? 'ory',
- );
- await _acceptOidcLoginAndRedirect();
+ // 401 응답은 세션이 없는 정상적인 상태이므로 예외로 처리하지 않고 우아하게 중단합니다.
+ final status = await AuthProxyService.getSessionStatus(useCookie: true);
+ if (status == 200) {
+ AuthTokenStore.setCookieMode(
+ provider: AuthTokenStore.getProvider() ?? 'ory',
+ );
+ await _acceptOidcLoginAndRedirect();
+ } else {
+ debugPrint(
+ "[Auth] OIDC auto-accept: No active session (status: $status)",
+ );
+ }
} catch (e) {
debugPrint("[Auth] OIDC auto-accept cookie check failed: $e");
}
diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock
index ae003ec3..fecd33f1 100644
--- a/userfront/pubspec.lock
+++ b/userfront/pubspec.lock
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
- version: "1.4.1"
+ version: "1.4.0"
cli_config:
dependency: transitive
description:
@@ -268,6 +268,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.2"
leak_tracker:
dependency: transitive
description:
@@ -320,18 +328,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
- version: "0.12.18"
+ version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
- version: "0.13.0"
+ version: "0.11.1"
meta:
dependency: transitive
description:
@@ -645,26 +653,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
+ sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev"
source: hosted
- version: "1.29.0"
+ version: "1.26.3"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.9"
+ version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
+ sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev"
source: hosted
- version: "0.6.15"
+ version: "0.6.12"
toml:
dependency: "direct main"
description: