diff --git a/docs/rp-auto-login-guide.md b/docs/rp-auto-login-guide.md index 9f001a70..fb9a9faa 100644 --- a/docs/rp-auto-login-guide.md +++ b/docs/rp-auto-login-guide.md @@ -59,17 +59,17 @@ Baron 계열 RP는 다음 fallback을 사용합니다. env URL이 설정되어 | --- | --- | --- | | `adminfront` | `ADMINFRONT_URL` | `${ADMINFRONT_URL}/login?auto=1` | | `devfront` | `DEVFRONT_URL` | `${DEVFRONT_URL}/login?auto=1&returnTo=%2Fclients` | -| `orgfront` | `ORGFRONT_URL` | `${ORGFRONT_URL}/login?auto=1` | +| `orgfront` | `ORGFRONT_URL` | `${ORGFRONT_URL}/login` | -orgfront는 `/login?auto=1` 진입 시 OIDC authorize 요청을 즉시 시작하며, 기본 callback 이후 이동 경로는 `/chart`입니다. +orgfront는 `/login` 진입부터 OIDC authorize 요청을 즉시 시작하며, 기본 callback 이후 이동 경로는 `/chart`입니다. 수동 로그인 화면 검증이 필요하면 `/login?auto=0`으로 자동 시작을 끌 수 있습니다. ## userfront 동작 userfront는 backend의 linked RP 응답을 기준으로 진입 URL을 선택합니다. 1. `status`가 active가 아니면 진입 URL을 만들지 않습니다. -2. `auto_login_supported=true`이면 `init_url`을 우선 사용합니다. -3. `init_url`이 없으면 `auto_login_url`을 사용합니다. +2. `auto_login_supported=true`이면 `auto_login_url`을 우선 사용합니다. +3. `auto_login_url`이 없으면 `init_url`을 사용합니다. 4. 자동 로그인 미지원이면 `init_url`이 있어도 기존 `url`로 이동합니다. 이 기준 때문에 `auto_login_supported=false`인 RP는 accidental auto-login을 수행하지 않습니다. diff --git a/orgfront/src/features/auth/AuthGuard.tsx b/orgfront/src/features/auth/AuthGuard.tsx index 6619506d..11c3d909 100644 --- a/orgfront/src/features/auth/AuthGuard.tsx +++ b/orgfront/src/features/auth/AuthGuard.tsx @@ -22,7 +22,13 @@ export default function AuthGuard() { } if (!auth.isAuthenticated) { - return ; + const returnTo = `${location.pathname}${location.search}`; + return ( + + ); } // 조직도 앱은 일반 사용자(user)도 볼 수 있어야 하므로 접근 제한을 해제합니다. diff --git a/orgfront/src/features/auth/LoginPage.tsx b/orgfront/src/features/auth/LoginPage.tsx index 477a764e..04fcd396 100644 --- a/orgfront/src/features/auth/LoginPage.tsx +++ b/orgfront/src/features/auth/LoginPage.tsx @@ -18,7 +18,7 @@ function LoginPage() { const [searchParams] = useSearchParams(); const autoStartedRef = useRef(false); const returnTo = searchParams.get("returnTo") || "/chart"; - const shouldAutoLogin = searchParams.get("auto") === "1"; + const shouldAutoLogin = searchParams.get("auto") !== "0"; useEffect(() => { if (auth.isAuthenticated) { diff --git a/orgfront/src/features/orgchart/userDisplay.ts b/orgfront/src/features/orgchart/userDisplay.ts index 044ccf33..ab6fa8aa 100644 --- a/orgfront/src/features/orgchart/userDisplay.ts +++ b/orgfront/src/features/orgchart/userDisplay.ts @@ -56,8 +56,8 @@ export function getOrgChartUserDisplayName( const { jobTitle, position } = getUserOrgProfile(user, tenant); const baseName = user.name.trim(); - if (jobTitle && position) return `${baseName} ${position}[${jobTitle}]`; - if (jobTitle) return `${baseName}[${jobTitle}]`; + if (jobTitle && position) return `${baseName}(${jobTitle}) ${position}`; + if (jobTitle) return `${baseName}(${jobTitle})`; if (position) return `${baseName} ${position}`; return baseName; } diff --git a/orgfront/tests/orgfront-auto-login.spec.ts b/orgfront/tests/orgfront-auto-login.spec.ts index 3b0b3464..8761ab9f 100644 --- a/orgfront/tests/orgfront-auto-login.spec.ts +++ b/orgfront/tests/orgfront-auto-login.spec.ts @@ -1,8 +1,6 @@ -import { expect, test } from "@playwright/test"; +import { expect, test, type Page } from "@playwright/test"; -test("orgfront login auto parameter starts OIDC authorization", async ({ - page, -}) => { +async function stubOidcAuthorization(page: Page) { let authorizationURL = ""; await page.route( @@ -32,11 +30,19 @@ test("orgfront login auto parameter starts OIDC authorization", async ({ }, ); - await page.goto("/login?auto=1&returnTo=%2Fpicker"); + return { + authorizationURL: () => authorizationURL, + }; +} - await expect.poll(() => authorizationURL).toContain("/oauth2/auth"); +test("orgfront login defaults to OIDC authorization", async ({ page }) => { + const oidc = await stubOidcAuthorization(page); - const parsed = new URL(authorizationURL); + await page.goto("/login"); + + await expect.poll(oidc.authorizationURL).toContain("/oauth2/auth"); + + const parsed = new URL(oidc.authorizationURL()); expect(parsed.searchParams.get("client_id")).toBe("orgfront"); expect(parsed.searchParams.get("redirect_uri")).toBe( "http://localhost:5175/auth/callback", @@ -44,3 +50,32 @@ test("orgfront login auto parameter starts OIDC authorization", async ({ expect(parsed.searchParams.get("response_type")).toBe("code"); expect(parsed.searchParams.get("scope") ?? "").toContain("openid"); }); + +test("orgfront login auto parameter starts OIDC authorization", async ({ + page, +}) => { + const oidc = await stubOidcAuthorization(page); + + await page.goto("/login?auto=1&returnTo=%2Fpicker"); + + await expect.poll(oidc.authorizationURL).toContain("/oauth2/auth"); + + const parsed = new URL(oidc.authorizationURL()); + expect(parsed.searchParams.get("client_id")).toBe("orgfront"); + expect(parsed.searchParams.get("redirect_uri")).toBe( + "http://localhost:5175/auth/callback", + ); + expect(parsed.searchParams.get("response_type")).toBe("code"); + expect(parsed.searchParams.get("scope") ?? "").toContain("openid"); +}); + +test("orgfront login can opt out of default OIDC authorization", async ({ + page, +}) => { + const oidc = await stubOidcAuthorization(page); + + await page.goto("/login?auto=0"); + await page.waitForTimeout(500); + + expect(oidc.authorizationURL()).toBe(""); +}); diff --git a/userfront/lib/features/dashboard/domain/linked_rp_launch.dart b/userfront/lib/features/dashboard/domain/linked_rp_launch.dart index f548dab6..ccc3fb1b 100644 --- a/userfront/lib/features/dashboard/domain/linked_rp_launch.dart +++ b/userfront/lib/features/dashboard/domain/linked_rp_launch.dart @@ -8,14 +8,14 @@ String? resolveLinkedRpLaunchUrl(LinkedRp rp) { } if (rp.autoLoginSupported) { - final initUrl = rp.initUrl.trim(); - if (initUrl.isNotEmpty) { - return initUrl; - } final autoLoginUrl = rp.autoLoginUrl.trim(); if (autoLoginUrl.isNotEmpty) { return autoLoginUrl; } + final initUrl = rp.initUrl.trim(); + if (initUrl.isNotEmpty) { + return initUrl; + } } final url = rp.url.trim(); diff --git a/userfront/test/linked_rp_launch_test.dart b/userfront/test/linked_rp_launch_test.dart index 05f6bf1e..d3b72b1d 100644 --- a/userfront/test/linked_rp_launch_test.dart +++ b/userfront/test/linked_rp_launch_test.dart @@ -43,22 +43,36 @@ void main() { expect(rp.autoLoginUrl, 'https://example.com/login?auto=1'); }); - test('자동 로그인 지원 앱은 initUrl을 우선 진입 URL로 사용한다', () { + test('자동 로그인 지원 앱은 autoLoginUrl을 우선 진입 URL로 사용한다', () { final launchUrl = resolveLinkedRpLaunchUrl( _linkedRp( status: 'active', url: 'https://example.com', initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', autoLoginSupported: true, + autoLoginUrl: 'https://example.com/login?auto=1', ), ); expect( launchUrl, - 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', + 'https://example.com/login?auto=1', ); }); + test('자동 로그인 지원 앱은 autoLoginUrl이 없으면 initUrl로 폴백한다', () { + final launchUrl = resolveLinkedRpLaunchUrl( + _linkedRp( + status: 'active', + url: 'https://example.com', + initUrl: 'https://example.com/login?auto=1', + autoLoginSupported: true, + ), + ); + + expect(launchUrl, 'https://example.com/login?auto=1'); + }); + test('자동 로그인 미지원 앱은 initUrl이 있어도 기존 url로 폴백한다', () { final launchUrl = resolveLinkedRpLaunchUrl( _linkedRp(