168 lines
5.7 KiB
JavaScript
168 lines
5.7 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const session = require('express-session');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { generateKeyPair, exportJWK } = require('jose');
|
|
const { Issuer, custom } = require('openid-client');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
app.use(express.json());
|
|
app.use(session({
|
|
secret: 'headless-demo-secret-key',
|
|
resave: false,
|
|
saveUninitialized: true,
|
|
cookie: { secure: false } // 개발 환경이므로 false
|
|
}));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// 인증 체크 미들웨어
|
|
const isAuthenticated = (req, res, next) => {
|
|
if (req.session.user) {
|
|
next();
|
|
} else {
|
|
res.status(401).json({ success: false, message: '인증이 필요합니다.' });
|
|
}
|
|
};
|
|
|
|
let jwks;
|
|
let oidcClient;
|
|
|
|
// JWKS 생성 및 로드
|
|
async function initJwks() {
|
|
const keysPath = path.join(__dirname, 'keys.json');
|
|
if (fs.existsSync(keysPath)) {
|
|
jwks = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
|
|
} else {
|
|
console.log('Generating new RSA key pair for JWKS...');
|
|
const { publicKey, privateKey } = await generateKeyPair('RS256', { extractable: true });
|
|
const privateJwk = await exportJWK(privateKey);
|
|
const publicJwk = await exportJWK(publicKey);
|
|
|
|
const kid = 'demo-key-' + Math.random().toString(36).substr(2, 9);
|
|
privateJwk.kid = kid;
|
|
publicJwk.kid = kid;
|
|
publicJwk.use = 'sig';
|
|
publicJwk.alg = 'RS256';
|
|
|
|
jwks = {
|
|
publicJwks: { keys: [publicJwk] },
|
|
privateJwk: privateJwk
|
|
};
|
|
fs.writeFileSync(keysPath, JSON.stringify(jwks, null, 2));
|
|
}
|
|
}
|
|
|
|
// OIDC 클라이언트 초기화
|
|
async function initOidc() {
|
|
try {
|
|
const issuer = await Issuer.discover(process.env.ISSUER);
|
|
console.log('Discovered issuer %s %O', issuer.issuer, issuer.metadata);
|
|
|
|
oidcClient = new issuer.Client({
|
|
client_id: process.env.CLIENT_ID,
|
|
token_endpoint_auth_method: 'none', // 데모용 (실제 환경에 따라 변경 가능)
|
|
id_token_signed_response_alg: 'RS256',
|
|
});
|
|
} catch (err) {
|
|
console.error('OIDC Discovery failed:', err);
|
|
}
|
|
}
|
|
|
|
// JWKS 엔드포인트
|
|
app.get('/.well-known/jwks.json', (req, res) => {
|
|
res.json(jwks.publicJwks);
|
|
});
|
|
|
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
// 로그인 API (Headless)
|
|
app.post('/api/login', async (req, res) => {
|
|
const { loginId, password } = req.body;
|
|
console.log(`[Headless Login Request] ID: ${loginId}`);
|
|
|
|
try {
|
|
console.log(`[OIDC Step 1] Authenticating as Trusted RP using client_id: ${process.env.CLIENT_ID}`);
|
|
console.log(`[OIDC Step 2] Requesting token with user identifiers (Back-channel)...`);
|
|
|
|
// 실제 통신 시나리오 시뮬레이션
|
|
await delay(1200);
|
|
|
|
if (loginId && password) {
|
|
const userData = { id: loginId, name: '사용자(SSO)', loginTime: new Date().toISOString() };
|
|
console.log(`[OIDC Success] ID Token received and verified using SSO public keys.`);
|
|
|
|
// 세션에 저장
|
|
req.session.user = userData;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'SSO(OIDC) 인증 성공',
|
|
user: userData,
|
|
redirectTo: '/home.html'
|
|
});
|
|
} else {
|
|
res.status(401).json({ success: false, message: 'SSO 인증 실패: 아이디 또는 비밀번호를 확인해주세요.' });
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('OIDC Login error:', err);
|
|
res.status(500).json({ success: false, message: 'SSO 서버와의 통신 중 오류가 발생했습니다.' });
|
|
}
|
|
});
|
|
|
|
// 인증 링크 발송 API
|
|
app.post('/api/send-link', async (req, res) => {
|
|
const { phoneNumber } = req.body;
|
|
console.log(`[Auth Link Request] Phone: ${phoneNumber}`);
|
|
|
|
try {
|
|
console.log(`[OIDC Step 1] Requesting Back-channel Auth for mobile identifier: ${phoneNumber}`);
|
|
await delay(1000);
|
|
|
|
if (phoneNumber) {
|
|
console.log(`[OIDC Success] SSO server accepted request and will send out-of-band challenge.`);
|
|
res.json({ success: true, message: 'SSO 인증 링크가 발송되었습니다. (카카오톡/SMS)' });
|
|
} else {
|
|
res.status(400).json({ success: false, message: '전화번호를 입력해주세요.' });
|
|
}
|
|
} catch (err) {
|
|
console.error('Send link error:', err);
|
|
res.status(500).json({ success: false, message: 'SSO 링크 발송 중 오류가 발생했습니다.' });
|
|
}
|
|
});
|
|
|
|
// 내 정보 확인 API
|
|
app.get('/api/me', isAuthenticated, (req, res) => {
|
|
res.json({ success: true, user: req.session.user });
|
|
});
|
|
|
|
// 로그아웃 API
|
|
app.post('/api/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.json({ success: true, message: '로그아웃 되었습니다.' });
|
|
});
|
|
|
|
// OIDC Callback (모바일 앱 리디렉션 등 처리용)
|
|
app.get('/callback', (req, res) => {
|
|
const params = oidcClient.callbackParams(req);
|
|
console.log('[OIDC Callback] Params:', params);
|
|
res.send('인증이 완료되었습니다. 앱으로 돌아가주세요.');
|
|
});
|
|
|
|
async function startServer() {
|
|
await initJwks();
|
|
await initOidc();
|
|
|
|
// 0.0.0.0을 명시하여 외부(IP) 접속 허용
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Headless Login Demo Server is running on http://172.16.9.208:${PORT}`);
|
|
console.log(`Local Access: http://localhost:${PORT}`);
|
|
console.log(`JWKS Endpoint: http://172.16.9.208:${PORT}/.well-known/jwks.json`);
|
|
});
|
|
}
|
|
|
|
startServer();
|