PM-fork 보안 인증 엔진 이식 및 한국어 단계별 로그 적용
This commit is contained in:
251
server.js
251
server.js
@@ -36,7 +36,7 @@ async function initJwks() {
|
|||||||
if (fs.existsSync(keysPath)) {
|
if (fs.existsSync(keysPath)) {
|
||||||
jwks = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
|
jwks = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
|
||||||
} else {
|
} else {
|
||||||
console.log('Generating new RSA key pair for JWKS...');
|
console.log('--- [시스템] 새로운 보안 키 쌍(RSA)을 생성하고 저장합니다. ---');
|
||||||
const { publicKey, privateKey } = await generateKeyPair('RS256', { extractable: true });
|
const { publicKey, privateKey } = await generateKeyPair('RS256', { extractable: true });
|
||||||
const privateJwk = await exportJWK(privateKey);
|
const privateJwk = await exportJWK(privateKey);
|
||||||
const publicJwk = await exportJWK(publicKey);
|
const publicJwk = await exportJWK(publicKey);
|
||||||
@@ -58,16 +58,17 @@ async function initJwks() {
|
|||||||
// OIDC 클라이언트 초기화
|
// OIDC 클라이언트 초기화
|
||||||
async function initOidc() {
|
async function initOidc() {
|
||||||
try {
|
try {
|
||||||
|
console.log(`--- [시스템] SSO 서버 정보를 조회 중입니다: ${process.env.ISSUER} ---`);
|
||||||
const issuer = await Issuer.discover(process.env.ISSUER);
|
const issuer = await Issuer.discover(process.env.ISSUER);
|
||||||
console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata);
|
console.log('--- [시스템] SSO 서버 연결 준비 완료. ---');
|
||||||
|
|
||||||
oidcClient = new issuer.Client({
|
oidcClient = new issuer.Client({
|
||||||
client_id: process.env.CLIENT_ID,
|
client_id: process.env.CLIENT_ID,
|
||||||
token_endpoint_auth_method: 'none', // 데모용 (실제 환경에 따라 변경 가능)
|
token_endpoint_auth_method: 'none',
|
||||||
id_token_signed_response_alg: 'RS256',
|
id_token_signed_response_alg: 'RS256',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('OIDC Discovery failed:', err);
|
console.error('--- [오류] SSO 서버 연결에 실패했습니다: ---', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,25 +103,28 @@ async function buildClientAssertion(clientId, audience) {
|
|||||||
app.post('/api/login', async (req, res) => {
|
app.post('/api/login', async (req, res) => {
|
||||||
const { loginId, password } = req.body;
|
const { loginId, password } = req.body;
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
console.log(`[PM-fork 방식 Headless OIDC] Attempting login for ID: ${loginId}`);
|
console.log(`\n================================================================`);
|
||||||
|
console.log(`[로그인 시작] 사번 ${loginId} 님에 대한 안전한 로그인을 시도합니다.`);
|
||||||
|
console.log(`================================================================`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const issuerInfo = await Issuer.discover(process.env.ISSUER);
|
const issuerInfo = await Issuer.discover(process.env.ISSUER);
|
||||||
const authEndpoint = issuerInfo.authorization_endpoint;
|
const authEndpoint = issuerInfo.authorization_endpoint;
|
||||||
const tokenEndpoint = issuerInfo.token_endpoint;
|
const tokenEndpoint = issuerInfo.token_endpoint;
|
||||||
|
const redirectUri = process.env.REDIRECT_URI;
|
||||||
|
|
||||||
// 1. Authorization Flow 시작 -> login_challenge 획득
|
// 1. Authorization Flow 시작 -> login_challenge 획득
|
||||||
|
console.log(`[1단계: 신호 요청] SSO 서버로부터 인증을 위한 고유 신호(Challenge)를 받아오고 있습니다.`);
|
||||||
const state = crypto.randomUUID();
|
const state = crypto.randomUUID();
|
||||||
const nonce = crypto.randomUUID();
|
const nonce = crypto.randomUUID();
|
||||||
const authUrl = new URL(authEndpoint);
|
const authUrl = new URL(authEndpoint);
|
||||||
authUrl.searchParams.set('client_id', process.env.CLIENT_ID);
|
authUrl.searchParams.set('client_id', process.env.CLIENT_ID);
|
||||||
authUrl.searchParams.set('redirect_uri', process.env.REDIRECT_URI);
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
authUrl.searchParams.set('response_type', 'code');
|
authUrl.searchParams.set('response_type', 'code');
|
||||||
authUrl.searchParams.set('scope', 'openid profile');
|
authUrl.searchParams.set('scope', 'openid profile');
|
||||||
authUrl.searchParams.set('state', state);
|
authUrl.searchParams.set('state', state);
|
||||||
authUrl.searchParams.set('nonce', nonce);
|
authUrl.searchParams.set('nonce', nonce);
|
||||||
|
|
||||||
console.log('[Step 1] Requesting login_challenge from authorization endpoint...');
|
|
||||||
const authRes = await fetch(authUrl.toString(), { redirect: 'manual' });
|
const authRes = await fetch(authUrl.toString(), { redirect: 'manual' });
|
||||||
const location = authRes.headers.get('location') || authRes.url;
|
const location = authRes.headers.get('location') || authRes.url;
|
||||||
|
|
||||||
@@ -128,102 +132,191 @@ app.post('/api/login', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
loginChallenge = new URL(location).searchParams.get('login_challenge');
|
loginChallenge = new URL(location).searchParams.get('login_challenge');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('리다이렉트 URL에서 login_challenge를 파싱할 수 없습니다.');
|
throw new Error('인증 신호 파싱 실패 (잘못된 경로)');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loginChallenge) {
|
if (!loginChallenge) {
|
||||||
throw new Error(`login_challenge를 획득할 수 없습니다. Location: ${location}`);
|
throw new Error(`인증 신호를 받지 못했습니다. (URL: ${location})`);
|
||||||
}
|
}
|
||||||
|
console.log(` --> 신호 획득 완료: ${loginChallenge.substring(0, 10)}...`);
|
||||||
|
|
||||||
// 쿠키 추출 (Ory 세션 유지를 위해 필요)
|
|
||||||
let cookies = authRes.headers.get('set-cookie') || '';
|
let cookies = authRes.headers.get('set-cookie') || '';
|
||||||
|
|
||||||
// 2. Headless Password API 호출 (client_assertion 사용)
|
// 2. Headless Password API 호출 (client_assertion 사용)
|
||||||
|
console.log(`[2단계: 본인 인증] 사번과 비밀번호를 보안 도장(Digital Signature)과 함께 서버에 전달합니다.`);
|
||||||
const headlessEndpoint = process.env.ISSUER.replace('/oidc', '/api/v1/auth/headless/password/login');
|
const headlessEndpoint = process.env.ISSUER.replace('/oidc', '/api/v1/auth/headless/password/login');
|
||||||
console.log(`[Step 2] Sending client_assertion to Headless API: ${headlessEndpoint}`);
|
|
||||||
|
|
||||||
const clientAssertion = await buildClientAssertion(process.env.CLIENT_ID, headlessEndpoint);
|
const audiences = [headlessEndpoint];
|
||||||
|
try {
|
||||||
|
const pathOnly = new URL(headlessEndpoint).pathname;
|
||||||
|
if (!audiences.includes(pathOnly)) audiences.push(pathOnly);
|
||||||
|
} catch (e) {}
|
||||||
|
if (!audiences.includes(process.env.ISSUER)) audiences.push(process.env.ISSUER);
|
||||||
|
|
||||||
|
let loginPayload;
|
||||||
|
let loginRes;
|
||||||
|
|
||||||
|
for (const aud of audiences) {
|
||||||
|
console.log(` --> 보안 검증 중 (대상: ${aud})`);
|
||||||
|
const clientAssertion = await buildClientAssertion(process.env.CLIENT_ID, aud);
|
||||||
|
|
||||||
|
loginRes = await fetch(headlessEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: process.env.CLIENT_ID,
|
||||||
|
client_assertion: clientAssertion,
|
||||||
|
login_challenge: loginChallenge,
|
||||||
|
loginId: loginId,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
loginPayload = await loginRes.json();
|
||||||
|
if (loginRes.ok) break;
|
||||||
|
|
||||||
|
const errorMsg = String(loginPayload.error || loginPayload.error_description || "").toLowerCase();
|
||||||
|
if (!errorMsg.includes('audience mismatch')) break;
|
||||||
|
console.log(` --> 보안 검증 대상을 조정하여 다시 시도합니다...`);
|
||||||
|
}
|
||||||
|
|
||||||
const loginRes = await fetch(headlessEndpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: process.env.CLIENT_ID,
|
|
||||||
client_assertion: clientAssertion,
|
|
||||||
login_challenge: loginChallenge,
|
|
||||||
loginId: loginId,
|
|
||||||
password: password
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginPayload = await loginRes.json();
|
|
||||||
if (!loginRes.ok) {
|
if (!loginRes.ok) {
|
||||||
throw new Error(loginPayload.message || loginPayload.error_description || 'Headless login API 거부됨');
|
console.error(' [실패] 서버에서 인증을 거부했습니다.');
|
||||||
|
console.error(' [상세 사유]:', JSON.stringify(loginPayload, null, 2));
|
||||||
|
throw new Error(loginPayload.message || loginPayload.error_description || 'ID/PW 인증 거부됨');
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectTo = loginPayload.redirectTo;
|
const redirectTo = loginPayload.redirectTo;
|
||||||
if (!redirectTo) throw new Error('응답에 redirectTo 필드가 없습니다.');
|
if (!redirectTo) throw new Error('인증 후 리다이렉트 정보를 받지 못했습니다.');
|
||||||
|
|
||||||
let newCookies = loginRes.headers.get('set-cookie');
|
let newCookies = loginRes.headers.get('set-cookie');
|
||||||
if (newCookies) cookies = cookies ? `${cookies}; ${newCookies}` : newCookies;
|
if (newCookies) cookies = cookies ? `${cookies}; ${newCookies}` : newCookies;
|
||||||
|
console.log(` --> 본인 인증 성공! 다음 단계로 이동합니다.`);
|
||||||
|
|
||||||
// 3. 리다이렉트를 따라가서 Authorization Code 획득
|
// 3. 리다이렉트를 따라가서 Authorization Code 획득 (Consent 자동 승인 포함)
|
||||||
console.log(`[Step 3] Following redirectTo to obtain authorization code...`);
|
console.log(`[3단계: 권한 획득] 인증 완료 후 필요한 권한(프로필 등)을 최종적으로 승인받는 과정입니다.`);
|
||||||
const redirectRes = await fetch(redirectTo, {
|
|
||||||
redirect: 'manual',
|
|
||||||
headers: { 'Cookie': cookies }
|
|
||||||
});
|
|
||||||
|
|
||||||
const callbackLocation = redirectRes.headers.get('location');
|
async function resolveRedirects(currentUrl, currentCookies, depth = 0) {
|
||||||
if (!callbackLocation || !callbackLocation.includes('code=')) {
|
if (depth > 10) throw new Error('연결 과정이 너무 깁니다. (최대 횟수 초과)');
|
||||||
throw new Error('Authorization code를 획득하지 못했습니다. (동의 화면(Consent)이 필요할 수 있습니다)');
|
|
||||||
|
console.log(` --> 경로 추적 중: ${new URL(currentUrl).pathname}`);
|
||||||
|
const res = await fetch(currentUrl, {
|
||||||
|
redirect: 'manual',
|
||||||
|
headers: { 'Cookie': currentCookies }
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextCookies = [currentCookies, ...(res.headers.getSetCookie?.() || [])].filter(Boolean).join('; ');
|
||||||
|
const location = res.headers.get('location');
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
if (currentUrl.includes('/consent')) {
|
||||||
|
console.log(' [자동 승인] 사용자의 정보 제공 동의 화면이 감지되어 시스템이 자동으로 승인 중입니다.');
|
||||||
|
const consentUrl = new URL(currentUrl);
|
||||||
|
const consentChallenge = consentUrl.searchParams.get('consent_challenge');
|
||||||
|
|
||||||
|
const detailsUrl = new URL('/api/v1/auth/consent', currentUrl);
|
||||||
|
detailsUrl.searchParams.set('consent_challenge', consentChallenge);
|
||||||
|
const detailsRes = await fetch(detailsUrl.toString(), {
|
||||||
|
headers: { 'Cookie': nextCookies }
|
||||||
|
});
|
||||||
|
const consentInfo = await detailsRes.json();
|
||||||
|
|
||||||
|
const acceptUrl = new URL('/api/v1/auth/consent/accept', currentUrl);
|
||||||
|
const acceptRes = await fetch(acceptUrl.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cookie': nextCookies
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
consent_challenge: consentChallenge,
|
||||||
|
grant_scope: consentInfo.requested_scope || ['openid', 'profile'],
|
||||||
|
grant_access_token_audience: consentInfo.requested_access_token_audience || []
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const acceptPayload = await acceptRes.json();
|
||||||
|
return resolveRedirects(acceptPayload.redirectTo, nextCookies, depth + 1);
|
||||||
|
}
|
||||||
|
throw new Error(`리다이렉트 지점을 찾을 수 없습니다. (URL: ${currentUrl})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteLocation = new URL(location, currentUrl).toString();
|
||||||
|
if (absoluteLocation.includes('code=')) {
|
||||||
|
return { code: new URL(absoluteLocation).searchParams.get('code'), cookies: nextCookies };
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveRedirects(absoluteLocation, nextCookies, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const callbackUrl = new URL(callbackLocation);
|
const { code, cookies: finalCookies } = await resolveRedirects(redirectTo, cookies);
|
||||||
const code = callbackUrl.searchParams.get('code');
|
console.log(` --> 임시 승인 코드 획득 완료.`);
|
||||||
|
|
||||||
// 4. Token Endpoint 호출하여 토큰 교환 (private_key_jwt 방식)
|
// 4. Token Endpoint 호출하여 토큰 교환 (유연한 인증 방식 적용)
|
||||||
console.log(`[Step 4] Exchanging authorization code for tokens...`);
|
console.log(`[4단계: 열쇠 발급] 획득한 코드를 실제 서비스 이용이 가능한 '보안 열쇠(Token)'로 교환합니다.`);
|
||||||
const tokenAssertion = await buildClientAssertion(process.env.CLIENT_ID, tokenEndpoint);
|
|
||||||
|
|
||||||
const tokenParams = new URLSearchParams();
|
async function exchangeToken(authMethod = 'private_key_jwt') {
|
||||||
tokenParams.set('grant_type', 'authorization_code');
|
const tokenParams = new URLSearchParams();
|
||||||
tokenParams.set('code', code);
|
tokenParams.set('grant_type', 'authorization_code');
|
||||||
tokenParams.set('client_id', process.env.CLIENT_ID);
|
tokenParams.set('code', code);
|
||||||
tokenParams.set('redirect_uri', process.env.REDIRECT_URI);
|
tokenParams.set('client_id', process.env.CLIENT_ID);
|
||||||
tokenParams.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
tokenParams.set('redirect_uri', redirectUri);
|
||||||
tokenParams.set('client_assertion', tokenAssertion);
|
|
||||||
|
if (authMethod === 'private_key_jwt') {
|
||||||
const tokenRes = await fetch(tokenEndpoint, {
|
console.log(' --> 가장 안전한 방식(보안 도장 포함)으로 열쇠를 요청합니다.');
|
||||||
method: 'POST',
|
const tokenAssertion = await buildClientAssertion(process.env.CLIENT_ID, tokenEndpoint);
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
tokenParams.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
||||||
body: tokenParams.toString()
|
tokenParams.set('client_assertion', tokenAssertion);
|
||||||
});
|
} else {
|
||||||
|
console.log(' --> 표준 방식(일반 클라이언트)으로 열쇠를 다시 요청합니다.');
|
||||||
const tokenData = await tokenRes.json();
|
}
|
||||||
if (!tokenRes.ok) throw new Error(tokenData.error_description || '토큰 교환 실패');
|
|
||||||
|
const res = await fetch(tokenEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: tokenParams.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
if (authMethod === 'private_key_jwt') {
|
||||||
|
console.log(' [안내] 서버가 엄격한 보안 도장을 요구하지 않습니다. 일반 방식으로 전환합니다.');
|
||||||
|
return exchangeToken('none');
|
||||||
|
}
|
||||||
|
throw new Error(data.error_description || data.error || '열쇠 교환 실패');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await exchangeToken();
|
||||||
|
console.log(` --> 보안 열쇠 발급 완료.`);
|
||||||
|
|
||||||
// 5. 성공: 토큰에서 사용자 정보 추출
|
// 5. 성공: 토큰에서 사용자 정보 추출
|
||||||
const idTokenPayload = JSON.parse(Buffer.from(tokenData.id_token.split('.')[1], 'base64url').toString());
|
const idTokenPayload = JSON.parse(Buffer.from(tokenData.id_token.split('.')[1], 'base64url').toString());
|
||||||
const userData = {
|
const userData = {
|
||||||
id: idTokenPayload.sub,
|
id: idTokenPayload.sub,
|
||||||
name: idTokenPayload.name || idTokenPayload.preferred_username || loginId,
|
name: idTokenPayload.name || idTokenPayload.preferred_username || loginId,
|
||||||
loginTime: new Date().toISOString(),
|
loginTime: new Date().toLocaleString('ko-KR'),
|
||||||
method: 'password (pm-fork-secure)'
|
method: 'PM-fork 방식(보안 강화형)'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[Step 5] Success! User authenticated: ${userData.name}`);
|
console.log(`\n[5단계: 로그인 완료] 모든 과정이 성공했습니다!`);
|
||||||
|
console.log(` - 환영합니다, ${userData.name} 님!`);
|
||||||
|
console.log(` - 로그인 시각: ${userData.loginTime}`);
|
||||||
|
console.log(`================================================================\n`);
|
||||||
|
|
||||||
req.session.user = userData;
|
req.session.user = userData;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'PM-fork 방식 인증 성공',
|
message: '성공적으로 로그인되었습니다.',
|
||||||
user: userData,
|
user: userData,
|
||||||
redirectTo: '/home.html'
|
redirectTo: '/home.html'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Headless OIDC Error]', err.message);
|
console.error(`\n[!!! 로그인 실패 !!!]`);
|
||||||
|
console.error(` - 사유: ${err.message}`);
|
||||||
|
console.log(`================================================================\n`);
|
||||||
res.status(500).json({ success: false, message: `로그인 실패: ${err.message}` });
|
res.status(500).json({ success: false, message: `로그인 실패: ${err.message}` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -231,35 +324,34 @@ app.post('/api/login', async (req, res) => {
|
|||||||
// 인증 링크 발송 API
|
// 인증 링크 발송 API
|
||||||
app.post('/api/send-link', async (req, res) => {
|
app.post('/api/send-link', async (req, res) => {
|
||||||
const { phoneNumber } = req.body;
|
const { phoneNumber } = req.body;
|
||||||
console.log(`[Auth Link Request] Phone: ${phoneNumber}`);
|
console.log(`\n[전화번호 인증 요청] 번호: ${phoneNumber}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[OIDC Step 1] Requesting Back-channel Auth for mobile identifier: ${phoneNumber}`);
|
console.log(` - 해당 번호로 일회성 인증 링크를 생성하고 있습니다.`);
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
|
|
||||||
if (phoneNumber) {
|
if (phoneNumber) {
|
||||||
console.log(`[OIDC Success] SSO server accepted request.`);
|
console.log(` - [성공] 가상의 인증 링크가 발송되었습니다.`);
|
||||||
|
|
||||||
// 데모를 위해 링크 발송 후 즉시 인증 완료된 것으로 간주하여 세션 생성
|
|
||||||
const userData = {
|
const userData = {
|
||||||
id: phoneNumber,
|
id: phoneNumber,
|
||||||
name: '사용자(휴대폰)',
|
name: '휴대폰 사용자',
|
||||||
loginTime: new Date().toISOString(),
|
loginTime: new Date().toLocaleString('ko-KR'),
|
||||||
method: 'phone'
|
method: '전화번호 인증(데모)'
|
||||||
};
|
};
|
||||||
req.session.user = userData;
|
req.session.user = userData;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '인증 링크가 발송되었으며, 시뮬레이션 인증이 완료되었습니다.',
|
message: '인증 링크가 발송되었으며, 테스트를 위해 인증이 즉시 완료되었습니다.',
|
||||||
redirectTo: '/home.html'
|
redirectTo: '/home.html'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({ success: false, message: '전화번호를 입력해주세요.' });
|
res.status(400).json({ success: false, message: '전화번호를 입력해주세요.' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Send link error:', err);
|
console.error('전화번호 인증 오류:', err);
|
||||||
res.status(500).json({ success: false, message: 'SSO 링크 발송 중 오류가 발생했습니다.' });
|
res.status(500).json({ success: false, message: '인증 과정 중 오류가 발생했습니다.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -271,13 +363,13 @@ app.get('/api/me', isAuthenticated, (req, res) => {
|
|||||||
// 로그아웃 API
|
// 로그아웃 API
|
||||||
app.post('/api/logout', (req, res) => {
|
app.post('/api/logout', (req, res) => {
|
||||||
req.session.destroy();
|
req.session.destroy();
|
||||||
|
console.log('[시스템] 사용자가 로그아웃했습니다.');
|
||||||
res.json({ success: true, message: '로그아웃 되었습니다.' });
|
res.json({ success: true, message: '로그아웃 되었습니다.' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// OIDC Callback (모바일 앱 리디렉션 등 처리용)
|
// OIDC Callback
|
||||||
app.get('/callback', (req, res) => {
|
app.get('/callback', (req, res) => {
|
||||||
const params = oidcClient.callbackParams(req);
|
console.log('[시스템] 콜백 엔드포인트에 접속했습니다.');
|
||||||
console.log('[OIDC Callback] Params:', params);
|
|
||||||
res.send('인증이 완료되었습니다. 앱으로 돌아가주세요.');
|
res.send('인증이 완료되었습니다. 앱으로 돌아가주세요.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,11 +377,12 @@ async function startServer() {
|
|||||||
await initJwks();
|
await initJwks();
|
||||||
await initOidc();
|
await initOidc();
|
||||||
|
|
||||||
// 0.0.0.0을 명시하여 외부(IP) 접속 허용
|
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`Headless Login Demo Server is running on http://172.16.9.208:${PORT}`);
|
console.log(`\n================================================================`);
|
||||||
console.log(`Local Access: http://localhost:${PORT}`);
|
console.log(`[서버 기동] Headless 로그인 데모 서버가 실행 중입니다.`);
|
||||||
console.log(`JWKS Endpoint: http://172.16.9.208:${PORT}/.well-known/jwks.json`);
|
console.log(`- 접속 주소: http://172.16.9.208:${PORT}`);
|
||||||
|
console.log(`- 로컬 주소: http://localhost:${PORT}`);
|
||||||
|
console.log(`================================================================\n`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user