forked from baron/baron-sso
Align RP auto login launch behavior
This commit is contained in:
@@ -59,17 +59,17 @@ Baron 계열 RP는 다음 fallback을 사용합니다. env URL이 설정되어
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `adminfront` | `ADMINFRONT_URL` | `${ADMINFRONT_URL}/login?auto=1` |
|
| `adminfront` | `ADMINFRONT_URL` | `${ADMINFRONT_URL}/login?auto=1` |
|
||||||
| `devfront` | `DEVFRONT_URL` | `${DEVFRONT_URL}/login?auto=1&returnTo=%2Fclients` |
|
| `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 동작
|
||||||
|
|
||||||
userfront는 backend의 linked RP 응답을 기준으로 진입 URL을 선택합니다.
|
userfront는 backend의 linked RP 응답을 기준으로 진입 URL을 선택합니다.
|
||||||
|
|
||||||
1. `status`가 active가 아니면 진입 URL을 만들지 않습니다.
|
1. `status`가 active가 아니면 진입 URL을 만들지 않습니다.
|
||||||
2. `auto_login_supported=true`이면 `init_url`을 우선 사용합니다.
|
2. `auto_login_supported=true`이면 `auto_login_url`을 우선 사용합니다.
|
||||||
3. `init_url`이 없으면 `auto_login_url`을 사용합니다.
|
3. `auto_login_url`이 없으면 `init_url`을 사용합니다.
|
||||||
4. 자동 로그인 미지원이면 `init_url`이 있어도 기존 `url`로 이동합니다.
|
4. 자동 로그인 미지원이면 `init_url`이 있어도 기존 `url`로 이동합니다.
|
||||||
|
|
||||||
이 기준 때문에 `auto_login_supported=false`인 RP는 accidental auto-login을 수행하지 않습니다.
|
이 기준 때문에 `auto_login_supported=false`인 RP는 accidental auto-login을 수행하지 않습니다.
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ export default function AuthGuard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!auth.isAuthenticated) {
|
if (!auth.isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />;
|
const returnTo = `${location.pathname}${location.search}`;
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={`/login?returnTo=${encodeURIComponent(returnTo)}`}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 조직도 앱은 일반 사용자(user)도 볼 수 있어야 하므로 접근 제한을 해제합니다.
|
// 조직도 앱은 일반 사용자(user)도 볼 수 있어야 하므로 접근 제한을 해제합니다.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function LoginPage() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const autoStartedRef = useRef(false);
|
const autoStartedRef = useRef(false);
|
||||||
const returnTo = searchParams.get("returnTo") || "/chart";
|
const returnTo = searchParams.get("returnTo") || "/chart";
|
||||||
const shouldAutoLogin = searchParams.get("auto") === "1";
|
const shouldAutoLogin = searchParams.get("auto") !== "0";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (auth.isAuthenticated) {
|
if (auth.isAuthenticated) {
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export function getOrgChartUserDisplayName(
|
|||||||
const { jobTitle, position } = getUserOrgProfile(user, tenant);
|
const { jobTitle, position } = getUserOrgProfile(user, tenant);
|
||||||
const baseName = user.name.trim();
|
const baseName = user.name.trim();
|
||||||
|
|
||||||
if (jobTitle && position) return `${baseName} ${position}[${jobTitle}]`;
|
if (jobTitle && position) return `${baseName}(${jobTitle}) ${position}`;
|
||||||
if (jobTitle) return `${baseName}[${jobTitle}]`;
|
if (jobTitle) return `${baseName}(${jobTitle})`;
|
||||||
if (position) return `${baseName} ${position}`;
|
if (position) return `${baseName} ${position}`;
|
||||||
return baseName;
|
return baseName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ({
|
async function stubOidcAuthorization(page: Page) {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
let authorizationURL = "";
|
let authorizationURL = "";
|
||||||
|
|
||||||
await page.route(
|
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("client_id")).toBe("orgfront");
|
||||||
expect(parsed.searchParams.get("redirect_uri")).toBe(
|
expect(parsed.searchParams.get("redirect_uri")).toBe(
|
||||||
"http://localhost:5175/auth/callback",
|
"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("response_type")).toBe("code");
|
||||||
expect(parsed.searchParams.get("scope") ?? "").toContain("openid");
|
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("");
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ String? resolveLinkedRpLaunchUrl(LinkedRp rp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rp.autoLoginSupported) {
|
if (rp.autoLoginSupported) {
|
||||||
final initUrl = rp.initUrl.trim();
|
|
||||||
if (initUrl.isNotEmpty) {
|
|
||||||
return initUrl;
|
|
||||||
}
|
|
||||||
final autoLoginUrl = rp.autoLoginUrl.trim();
|
final autoLoginUrl = rp.autoLoginUrl.trim();
|
||||||
if (autoLoginUrl.isNotEmpty) {
|
if (autoLoginUrl.isNotEmpty) {
|
||||||
return autoLoginUrl;
|
return autoLoginUrl;
|
||||||
}
|
}
|
||||||
|
final initUrl = rp.initUrl.trim();
|
||||||
|
if (initUrl.isNotEmpty) {
|
||||||
|
return initUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = rp.url.trim();
|
final url = rp.url.trim();
|
||||||
|
|||||||
@@ -43,22 +43,36 @@ void main() {
|
|||||||
expect(rp.autoLoginUrl, 'https://example.com/login?auto=1');
|
expect(rp.autoLoginUrl, 'https://example.com/login?auto=1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('자동 로그인 지원 앱은 initUrl을 우선 진입 URL로 사용한다', () {
|
test('자동 로그인 지원 앱은 autoLoginUrl을 우선 진입 URL로 사용한다', () {
|
||||||
final launchUrl = resolveLinkedRpLaunchUrl(
|
final launchUrl = resolveLinkedRpLaunchUrl(
|
||||||
_linkedRp(
|
_linkedRp(
|
||||||
status: 'active',
|
status: 'active',
|
||||||
url: 'https://example.com',
|
url: 'https://example.com',
|
||||||
initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
|
initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1',
|
||||||
autoLoginSupported: true,
|
autoLoginSupported: true,
|
||||||
|
autoLoginUrl: 'https://example.com/login?auto=1',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
launchUrl,
|
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로 폴백한다', () {
|
test('자동 로그인 미지원 앱은 initUrl이 있어도 기존 url로 폴백한다', () {
|
||||||
final launchUrl = resolveLinkedRpLaunchUrl(
|
final launchUrl = resolveLinkedRpLaunchUrl(
|
||||||
_linkedRp(
|
_linkedRp(
|
||||||
|
|||||||
Reference in New Issue
Block a user