diff --git a/package-lock.json b/package-lock.json index e7860d2..cbea398 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "dotenv": "^17.4.1", "express": "^5.2.1", + "express-session": "^1.19.0", "jose": "^6.2.2", "openid-client": "^5.7.1" } @@ -285,6 +286,50 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "dependencies": { + "cookie": "~0.7.2", + "cookie-signature": "~1.0.7", + "debug": "~2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "~5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -596,6 +641,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -676,6 +730,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -716,6 +779,26 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -877,6 +960,18 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 957f9f4..08f8234 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "dotenv": "^17.4.1", "express": "^5.2.1", + "express-session": "^1.19.0", "jose": "^6.2.2", "openid-client": "^5.7.1" } diff --git a/public/app.js b/public/app.js index 885045e..d080eeb 100644 --- a/public/app.js +++ b/public/app.js @@ -101,7 +101,13 @@ loginForm.addEventListener('submit', async (e) => { successTitle.textContent = currentMode === 'phone' ? '인증링크 발송 완료' : '로그인 성공'; successDescription.textContent = currentMode === 'phone' ? `${identifier} 번호로 인증 링크를 보냈습니다.` - : `${identifier} 계정으로 접속되었습니다.`; + : `${identifier} 계정으로 접속되었습니다. 잠시 후 홈 화면으로 이동합니다...`; + + if (result.redirectTo) { + setTimeout(() => { + window.location.href = result.redirectTo; + }, 1500); + } } else { alert(result.message || '오류가 발생했습니다.'); submitButton.disabled = false; diff --git a/public/home.html b/public/home.html new file mode 100644 index 0000000..f946ca2 --- /dev/null +++ b/public/home.html @@ -0,0 +1,75 @@ + + + + + + Home - Headless Login Demo + + + + +
+

환영합니다!

+

SSO를 통해 안전하게 로그인되었습니다.

+ +
+

사용자 정보를 불러오는 중...

+
+ + +
+ + + + diff --git a/server.js b/server.js index 4aeae13..6cbfbd2 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,6 @@ 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'); @@ -9,8 +10,23 @@ 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; @@ -75,12 +91,17 @@ app.post('/api/login', async (req, res) => { 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: { id: loginId, name: '사용자(SSO)' }, - redirectTo: '/home' + user: userData, + redirectTo: '/home.html' }); } else { res.status(401).json({ success: false, message: 'SSO 인증 실패: 아이디 또는 비밀번호를 확인해주세요.' }); @@ -113,6 +134,17 @@ app.post('/api/send-link', async (req, res) => { } }); +// 내 정보 확인 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);