BARON-SSO 로그인 API 요청경로 수정
All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 12s
ITAM Docker Build Check / docker-build-check (push) Successful in 23s

This commit is contained in:
2026-07-01 17:47:20 +09:00
parent 2512082402
commit fd0bd126d1
4 changed files with 550 additions and 30 deletions

117
server.js
View File

@@ -16,7 +16,10 @@ const {
REDIRECT_URI,
JWKS_URI,
SESSION_SECRET,
ERROR_LOCALE_PATH
ERROR_LOCALE_PATH,
PHONE_HEADLESS_LINK_INIT_ENDPOINT,
PHONE_HEADLESS_LINK_POLL_ENDPOINT,
PHONE_HEADLESS_LOGIN_ENDPOINT
} = process.env;
const SESSION_SECRET_VALUE = SESSION_SECRET || 'itam-headless-session-secret';
@@ -207,6 +210,8 @@ const ensureSsoConfig = () => {
}
};
const buildIssuerEndpoint = (overrideUrl, fallbackPath) => new URL(overrideUrl || fallbackPath, ISSUER).toString();
const base64Url = (input) => Buffer.from(input).toString('base64url');
const sha256Base64Url = (input) => crypto.createHash('sha256').update(input).digest('base64url');
@@ -561,9 +566,36 @@ const runHeadlessSsoLogin = async ({ loginId, password }) => {
};
};
const resolveAuthenticatedPhoneLogin = async ({ redirectTo, cookies, discovery, authState }) => {
const resolution = await resolveRedirects(redirectTo, cookies);
if (resolution.isErrorRedirect) {
return { status: 'error_redirect', redirectTo: resolution.finalUrl };
}
if (!resolution.code) {
throw new Error('Authorization code not found after phone redirect resolution');
}
const tokenResponse = await exchangeAuthorizationCode(
resolution.code,
discovery,
authState.codeVerifier
);
const idTokenPayload = decodeJwtPayload(tokenResponse.id_token);
return {
status: 'authenticated',
tokens: tokenResponse,
profile: idTokenPayload
};
};
const initHeadlessPhoneLogin = async ({ loginId }) => {
const { discovery, cookies, loginChallenge, authState } = await beginAuthorizationFlow();
const headlessEndpoint = new URL('/api/v1/auth/headless/link/init', ISSUER).toString();
const headlessEndpoint = buildIssuerEndpoint(
PHONE_HEADLESS_LOGIN_ENDPOINT || PHONE_HEADLESS_LINK_INIT_ENDPOINT,
'/api/v1/auth/headless/phone-login'
);
const initRes = await fetch(headlessEndpoint, {
method: 'POST',
@@ -581,11 +613,25 @@ const initHeadlessPhoneLogin = async ({ loginId }) => {
const nextCookies = appendCookies(cookies, initRes);
const initBody = await parseJsonSafely(initRes);
if (!initRes.ok || !initBody?.pendingRef) {
if (!initRes.ok) {
throw new Error(`Phone link init failed: ${initRes.status} ${JSON.stringify(initBody)}`);
}
if (initBody?.redirectTo) {
return resolveAuthenticatedPhoneLogin({
redirectTo: initBody.redirectTo,
cookies: nextCookies,
discovery,
authState
});
}
if (!initBody?.pendingRef) {
throw new Error(`Phone link init failed: ${initRes.status} ${JSON.stringify(initBody)}`);
}
return {
status: 'pending',
discovery,
cookies: nextCookies,
pendingRef: initBody.pendingRef,
@@ -597,7 +643,10 @@ const initHeadlessPhoneLogin = async ({ loginId }) => {
};
const pollHeadlessPhoneLogin = async (pendingContext) => {
const pollEndpoint = new URL('/api/v1/auth/headless/link/poll', ISSUER).toString();
const pollEndpoint = buildIssuerEndpoint(
PHONE_HEADLESS_LINK_POLL_ENDPOINT,
'/api/v1/auth/headless/link/poll'
);
const pollRes = await fetch(pollEndpoint, {
method: 'POST',
headers: {
@@ -615,27 +664,12 @@ const pollHeadlessPhoneLogin = async (pendingContext) => {
const pollBody = await parseJsonSafely(pollRes);
if (pollRes.ok && pollBody?.redirectTo) {
const resolution = await resolveRedirects(pollBody.redirectTo, nextCookies);
if (resolution.isErrorRedirect) {
return { status: 'error_redirect', redirectTo: resolution.finalUrl };
}
if (!resolution.code) {
throw new Error('Authorization code not found after phone redirect resolution');
}
const tokenResponse = await exchangeAuthorizationCode(
resolution.code,
pendingContext.discovery,
pendingContext.authState.codeVerifier
);
const idTokenPayload = decodeJwtPayload(tokenResponse.id_token);
return {
status: 'authenticated',
tokens: tokenResponse,
profile: idTokenPayload
};
return resolveAuthenticatedPhoneLogin({
redirectTo: pollBody.redirectTo,
cookies: nextCookies,
discovery: pendingContext.discovery,
authState: pendingContext.authState
});
}
const statusCode = pollBody?.code || pollBody?.error;
@@ -745,17 +779,40 @@ app.post('/api/auth/headless/phone/init', async (req, res) => {
}
try {
const pendingLogin = await initHeadlessPhoneLogin({ loginId });
const phoneLoginResult = await initHeadlessPhoneLogin({ loginId });
if (phoneLoginResult.status === 'error_redirect') {
return res.status(403).json({ redirectTo: phoneLoginResult.redirectTo, code: 'tenant_not_allowed' });
}
if (phoneLoginResult.status === 'authenticated') {
req.session.user = {
loginId,
profile: phoneLoginResult.profile,
tokens: {
accessToken: phoneLoginResult.tokens.access_token,
idToken: phoneLoginResult.tokens.id_token,
expiresIn: phoneLoginResult.tokens.expires_in,
scope: phoneLoginResult.tokens.scope,
tokenType: phoneLoginResult.tokens.token_type
}
};
registerSessionIdentity(req.sessionID, req.session.user);
await saveSession(req);
return res.json({ success: true, status: 'authenticated', user: req.session.user });
}
req.session.pendingPhoneLogin = {
...pendingLogin,
...phoneLoginResult,
startedAt: Date.now()
};
await saveSession(req);
res.json({
success: true,
pendingRef: pendingLogin.pendingRef,
intervalMs: pendingLogin.intervalMs,
expiresInMs: pendingLogin.expiresInMs,
status: 'pending',
pendingRef: phoneLoginResult.pendingRef,
intervalMs: phoneLoginResult.intervalMs,
expiresInMs: phoneLoginResult.expiresInMs,
message: '인증 링크를 발송했습니다. 모바일에서 승인 후 잠시만 기다려주세요.'
});
} catch (error) {