feat: SSO(OIDC) 연동 및 Trusted RP 기능 구현 (Phase 2)
- openid-client 기반 SSO Discovery 및 클라이언트 초기화 - jose를 이용한 동적 JWKS 생성 및 엔드포인트(/well-known/jwks.json) 구현 - OIDC 백채널 인증 흐름 시뮬레이션 및 상세 로그 추가 - .env 기반 환경 설정 및 보안 처리
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
.env
|
||||
keys.json
|
||||
.DS_Store
|
||||
*.log
|
||||
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -9,7 +9,10 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1"
|
||||
"dotenv": "^17.4.1",
|
||||
"express": "^5.2.1",
|
||||
"jose": "^6.2.2",
|
||||
"openid-client": "^5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -153,6 +156,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
|
||||
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -448,6 +463,27 @@
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -518,6 +554,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -530,6 +575,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
||||
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -551,6 +605,30 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jose": "^4.15.9",
|
||||
"lru-cache": "^6.0.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"oidc-token-hash": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/jose": {
|
||||
"version": "4.15.9",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -822,6 +900,12 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1"
|
||||
"dotenv": "^17.4.1",
|
||||
"express": "^5.2.1",
|
||||
"jose": "^6.2.2",
|
||||
"openid-client": "^5.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
135
server.js
135
server.js
@@ -1,5 +1,9 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
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;
|
||||
@@ -7,36 +11,123 @@ const PORT = process.env.PORT || 3000;
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Mock Login API
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { loginId, password } = req.body;
|
||||
console.log(`[Login Attempt] ID: ${loginId}, PW: ${password}`);
|
||||
let jwks;
|
||||
let oidcClient;
|
||||
|
||||
// Simulate network delay
|
||||
setTimeout(() => {
|
||||
if (loginId && password) {
|
||||
res.json({ success: true, message: '로그인 성공', redirectTo: '/home' });
|
||||
} else {
|
||||
res.status(400).json({ success: false, message: 'ID와 비밀번호를 모두 입력해주세요.' });
|
||||
}
|
||||
}, 800);
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Mock Send Authentication Link API
|
||||
app.post('/api/send-link', (req, res) => {
|
||||
const { phoneNumber } = req.body;
|
||||
console.log(`[Link Request] Phone: ${phoneNumber}`);
|
||||
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) {
|
||||
console.log(`[OIDC Success] ID Token received and verified using SSO public keys.`);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'SSO(OIDC) 인증 성공',
|
||||
user: { id: loginId, name: '사용자(SSO)' },
|
||||
redirectTo: '/home'
|
||||
});
|
||||
} 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);
|
||||
|
||||
// Simulate network delay
|
||||
setTimeout(() => {
|
||||
if (phoneNumber) {
|
||||
res.json({ success: true, message: '인증링크가 발송되었습니다.' });
|
||||
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: '전화번호를 입력해주세요.' });
|
||||
}
|
||||
}, 800);
|
||||
} catch (err) {
|
||||
console.error('Send link error:', err);
|
||||
res.status(500).json({ success: false, message: 'SSO 링크 발송 중 오류가 발생했습니다.' });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Headless Login Demo Server is running on http://localhost:${PORT}`);
|
||||
// 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();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Headless Login Demo Server is running on http://172.16.9.208:${PORT}`);
|
||||
console.log(`JWKS Endpoint: http://172.16.9.208:${PORT}/.well-known/jwks.json`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
Reference in New Issue
Block a user