forked from baron/baron-sso
조직도 M2M조회 추가, 자동로그인 보완
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AlertTriangle, ExternalLink, LogIn, ShieldHalf } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
@@ -11,14 +11,30 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { canStartBrowserPkceLogin } from "../../lib/authConfig";
|
||||
|
||||
const insecurePkceMessage =
|
||||
"이 주소에서는 브라우저 보안 정책 때문에 SSO 로그인을 시작할 수 없습니다. HTTPS 또는 localhost로 접속하거나, 내부망/host.docker.internal 개발 접속은 Chrome의 insecure-origin secure context 옵션에 실제 auth UI origin(예: http://host.docker.internal:5000)을 정확히 등록해 주세요.";
|
||||
|
||||
function LoginPage() {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const autoStartedRef = useRef(false);
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
const returnTo = searchParams.get("returnTo") || "/clients";
|
||||
const shouldAutoLogin = searchParams.get("auto") === "1";
|
||||
const authErrorMessage = useMemo(() => {
|
||||
const message = auth.error?.message;
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
if (message.includes("Crypto.subtle")) {
|
||||
return insecurePkceMessage;
|
||||
}
|
||||
return message;
|
||||
}, [auth.error?.message]);
|
||||
const visibleLoginError = loginError || authErrorMessage;
|
||||
|
||||
useEffect(() => {
|
||||
if (auth.isAuthenticated) {
|
||||
@@ -33,6 +49,10 @@ function LoginPage() {
|
||||
if (autoStartedRef.current || auth.isLoading || auth.activeNavigator) {
|
||||
return;
|
||||
}
|
||||
if (!canStartBrowserPkceLogin()) {
|
||||
setLoginError(insecurePkceMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
autoStartedRef.current = true;
|
||||
void auth.signinRedirect({
|
||||
@@ -44,6 +64,11 @@ function LoginPage() {
|
||||
|
||||
const handleSSOLogin = async () => {
|
||||
try {
|
||||
setLoginError(null);
|
||||
if (!canStartBrowserPkceLogin()) {
|
||||
setLoginError(insecurePkceMessage);
|
||||
return;
|
||||
}
|
||||
await auth.signinRedirect({
|
||||
state: {
|
||||
returnTo: "/clients",
|
||||
@@ -99,6 +124,16 @@ function LoginPage() {
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{visibleLoginError ? (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm leading-5 text-destructive"
|
||||
>
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span>{visibleLoginError}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
||||
개발자 포털 세션은 브라우저 정책에 따라 유지됩니다.
|
||||
<br />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEVFRONT_AUTH_CALLBACK_PATH,
|
||||
buildDevFrontAuthRedirectUris,
|
||||
canStartBrowserPkceLogin,
|
||||
resolveDevFrontPublicOrigin,
|
||||
} from "./authConfig";
|
||||
|
||||
@@ -26,4 +27,69 @@ describe("devfront auth config", () => {
|
||||
it("keeps the callback path aligned with the registered redirect path", () => {
|
||||
expect(DEVFRONT_AUTH_CALLBACK_PATH).toBe("/auth/callback");
|
||||
});
|
||||
|
||||
it("blocks browser PKCE login in an insecure context", () => {
|
||||
expect(
|
||||
canStartBrowserPkceLogin({
|
||||
isSecureContext: false,
|
||||
origin: "http://172.16.9.189:5174",
|
||||
cryptoSubtleAvailable: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canStartBrowserPkceLogin({
|
||||
isSecureContext: true,
|
||||
origin: "http://172.16.9.189:5174",
|
||||
cryptoSubtleAvailable: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("allows host.docker.internal when WebCrypto is enabled by the browser", () => {
|
||||
expect(
|
||||
canStartBrowserPkceLogin({
|
||||
isSecureContext: false,
|
||||
origin: "http://host.docker.internal:5000",
|
||||
cryptoSubtleAvailable: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
canStartBrowserPkceLogin({
|
||||
isSecureContext: false,
|
||||
origin: "http://host.docker.internal:5000",
|
||||
cryptoSubtleAvailable: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("allows private network IPv4 origins when WebCrypto is enabled by the browser", () => {
|
||||
for (const origin of [
|
||||
"http://10.0.0.10:5000",
|
||||
"http://172.16.9.189:5000",
|
||||
"http://172.31.255.255:5000",
|
||||
"http://192.168.0.20:5000",
|
||||
]) {
|
||||
expect(
|
||||
canStartBrowserPkceLogin({
|
||||
isSecureContext: false,
|
||||
origin,
|
||||
cryptoSubtleAvailable: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
for (const origin of [
|
||||
"http://172.15.255.255:5000",
|
||||
"http://172.32.0.1:5000",
|
||||
"http://8.8.8.8:5000",
|
||||
]) {
|
||||
expect(
|
||||
canStartBrowserPkceLogin({
|
||||
isSecureContext: false,
|
||||
origin,
|
||||
cryptoSubtleAvailable: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,3 +31,54 @@ export function buildDevFrontAuthRedirectUris(
|
||||
popupRedirectUri: `${publicOrigin}${DEVFRONT_AUTH_CALLBACK_PATH}`,
|
||||
};
|
||||
}
|
||||
|
||||
export type BrowserPkceLoginCheck = {
|
||||
isSecureContext?: boolean;
|
||||
origin?: string;
|
||||
cryptoSubtleAvailable?: boolean;
|
||||
};
|
||||
|
||||
const devTrustedPkceHosts = new Set([
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"host.docker.internal",
|
||||
]);
|
||||
|
||||
function isPrivateIPv4(hostname: string) {
|
||||
const parts = hostname.split(".").map((part) => Number.parseInt(part, 10));
|
||||
if (
|
||||
parts.length !== 4 ||
|
||||
parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [first, second] = parts;
|
||||
return (
|
||||
first === 10 ||
|
||||
(first === 172 && second >= 16 && second <= 31) ||
|
||||
(first === 192 && second === 168)
|
||||
);
|
||||
}
|
||||
|
||||
function isDevTrustedPkceOrigin(origin: string) {
|
||||
try {
|
||||
const hostname = new URL(origin).hostname;
|
||||
return devTrustedPkceHosts.has(hostname) || isPrivateIPv4(hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function canStartBrowserPkceLogin({
|
||||
isSecureContext = window.isSecureContext,
|
||||
origin = window.location.origin,
|
||||
cryptoSubtleAvailable = Boolean(window.crypto?.subtle),
|
||||
}: BrowserPkceLoginCheck = {}) {
|
||||
if (isSecureContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isDevTrustedPkceOrigin(origin) && cryptoSubtleAvailable;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user