Compare commits

..

4 Commits

Author SHA1 Message Date
kyy
5d1bad39f2 sso 인증 기능 구현 2026-01-16 13:15:41 +09:00
kyy
d5179daf57 UI 개선 및 스타일 적용 2026-01-16 13:15:08 +09:00
kyy
dbad6bccf4 컨테이너 변경 및 .env 적용 2026-01-16 13:09:44 +09:00
kyy
b1c3605df0 JWT_SECRET 설정 변수 추가 2026-01-16 13:08:38 +09:00
10 changed files with 692 additions and 86 deletions

1
.env.sample Normal file
View File

@@ -0,0 +1 @@
JWT_SECRET=your_jwt_secret_key_here

View File

@@ -1,15 +1,17 @@
version: '3.8' version: "3.8"
services: services:
web: web:
build: build:
context: ./sso-demo context: ./sso-demo
dockerfile: ../Dockerfile dockerfile: ../Dockerfile
container_name: sso-demo-container container_name: sso-express
ports: ports:
- "3000:3000" - "3000:3000"
volumes: volumes:
- ./sso-demo:/usr/src/app - ./sso-demo:/usr/src/app
- /usr/src/app/node_modules - /usr/src/app/node_modules
env_file:
- .env
environment: environment:
- NODE_ENV=development - NODE_ENV=development

View File

@@ -1,3 +1,4 @@
require('dotenv').config();
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
const path = require('path'); const path = require('path');
@@ -12,7 +13,7 @@ app.set('view engine', 'ejs');
// Session middleware setup // Session middleware setup
app.use(session({ app.use(session({
secret: 'a-very-secret-key-that-should-be-in-env-vars', // In production, use environment variables secret: process.env.JWT_SECRET, // In production, use environment variables
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { secure: false } // In production, set secure: true if using HTTPS cookie: { secure: false } // In production, set secure: true if using HTTPS

View File

@@ -1,51 +1,120 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const User = require('../models/user'); const User = require('../models/user');
const axios = require('axios');
// Cache for JWKS clients to avoid re-fetching the configuration on every request.
const jwksClients = {};
// Function to get the JWKS URI from the OIDC discovery endpoint.
async function getJwksUri(issuer) {
try {
const discoveryUrl = `${issuer}/.well-known/openid-configuration`;
console.log(`Fetching OIDC discovery document from: ${discoveryUrl}`);
const response = await axios.get(discoveryUrl);
console.log(`Successfully fetched OIDC discovery document. JWKS URI: ${response.data.jwks_uri}`);
return response.data.jwks_uri;
} catch (error) {
console.error('Failed to fetch OIDC discovery document:', error.message);
throw new Error('Could not fetch JWKS URI.');
}
}
function fetchKey(client, header, callback) {
console.log(`Fetching signing key for kid: ${header.kid}`);
client.getSigningKey(header.kid, (err, key) => {
if (err) {
console.error('Error getting signing key:', err);
return callback(err);
}
const signingKey = key.publicKey || key.rsaPublicKey;
console.log('Successfully fetched signing key.');
callback(null, signingKey);
});
}
async function ssoHandler(req, res, next) { async function ssoHandler(req, res, next) {
// 1. Check if the token is in the query parameters console.log('ssoHandler started.');
const { token } = req.query; const { token } = req.query;
if (token) { if (token) {
try { console.log('Token found in query. Verifying...');
// 2. Decode the JWT to get the payload const untrustedDecoded = jwt.decode(token, { complete: true });
// In a real app, you MUST verify the token signature using jwt.verify()
// For this demo, we'll just decode to inspect the payload.
const decoded = jwt.decode(token);
if (!untrustedDecoded) {
return res.status(400).send('Invalid token.');
}
const issuer = untrustedDecoded.payload.iss;
let client = jwksClients[issuer];
if (!client) {
try {
console.log(`Creating new JWKS client for issuer: ${issuer}`);
const jwksUri = await getJwksUri(issuer);
client = jwksClient({
jwksUri: jwksUri,
cache: true,
rateLimit: true,
});
jwksClients[issuer] = client;
} catch (err) {
console.error('Could not create JWKS client.', err);
return res.status(500).send('An error occurred during SSO processing due to a configuration error.');
}
} else {
console.log(`Using existing JWKS client for issuer: ${issuer}`);
}
jwt.verify(token, (header, callback) => fetchKey(client, header, callback), { algorithms: ['RS256'] }, async (err, decoded) => {
if (err) {
console.error('JWT verification failed:', err);
return res.status(500).send('An error occurred during SSO processing due to a verification failure.');
}
console.log('JWT verified successfully. Decoded token:', decoded);
try {
if (!decoded || !decoded.sub) { if (!decoded || !decoded.sub) {
console.error('Invalid token: "sub" claim is missing.');
return res.status(400).send('Invalid token: "sub" claim is missing.'); return res.status(400).send('Invalid token: "sub" claim is missing.');
} }
// 3. Find user by 'sub' claim console.log(`Looking for user with sso_sub: ${decoded.sub}`);
let user = await User.findBySsoSub(decoded.sub); let user = await User.findBySsoSub(decoded.sub);
// 4. If user doesn't exist, create a new one (auto-registration)
if (!user) { if (!user) {
console.log('User not found. Creating a new user.');
user = await User.createUser({ sso_sub: decoded.sub }); user = await User.createUser({ sso_sub: decoded.sub });
console.log('New user created:', user);
} else {
console.log('User found:', user);
} }
// 5. Save user information in the session
req.session.userId = user.id; req.session.userId = user.id;
console.log(`User ID ${user.id} stored in session.`);
// 6. Redirect to the same URL without the token parameter
const redirectUrl = req.path; const redirectUrl = req.path;
console.log(`Redirecting to ${redirectUrl}`);
return res.redirect(redirectUrl); return res.redirect(redirectUrl);
} catch (error) { } catch (error) {
console.error('SSO handling failed:', error); console.error('Error during user lookup or creation:', error);
return res.status(500).send('An error occurred during SSO processing.'); return res.status(500).send('An error occurred during user processing.');
} }
} });
} else {
// Attach user to request if session exists console.log('No token in query. Proceeding to next middleware.');
if (req.session.userId) { if (req.session.userId) {
console.log(`Session found for user ID: ${req.session.userId}`);
res.locals.user = await User.findById(req.session.userId); res.locals.user = await User.findById(req.session.userId);
} else { } else {
console.log('No session found.');
res.locals.user = null; res.locals.user = null;
} }
return next(); return next();
} }
}
module.exports = ssoHandler; module.exports = ssoHandler;

View File

@@ -9,13 +9,138 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"dotenv": "^17.2.3",
"ejs": "^4.0.1", "ejs": "^4.0.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-session": "^1.18.2", "express-session": "^1.18.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"jwks-rsa": "^3.2.0",
"nodemon": "^3.1.11" "nodemon": "^3.1.11"
} }
}, },
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.25",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "^1"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.19.8",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
"integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz",
"integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"license": "MIT"
},
"node_modules/@types/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "<1"
}
},
"node_modules/@types/serve-static/node_modules/@types/send": {
"version": "0.17.6",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -48,6 +173,23 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -179,6 +321,18 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -242,6 +396,15 @@
} }
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -251,6 +414,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -334,6 +509,21 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -474,6 +664,63 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -597,6 +844,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -731,6 +993,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"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/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "9.0.3", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
@@ -764,6 +1035,23 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/jwks-rsa": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz",
"integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==",
"license": "MIT",
"dependencies": {
"@types/express": "^4.17.20",
"@types/jsonwebtoken": "^9.0.4",
"debug": "^4.3.4",
"jose": "^4.15.4",
"limiter": "^1.1.5",
"lru-memoizer": "^2.2.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/jws": { "node_modules/jws": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
@@ -774,6 +1062,17 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -816,6 +1115,28 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT" "license": "MIT"
}, },
"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/lru-memoizer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
"license": "MIT",
"dependencies": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "6.0.0"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1049,6 +1370,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": { "node_modules/pstree.remy": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -1387,6 +1714,12 @@
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1410,6 +1743,12 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "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"
} }
} }
} }

View File

@@ -11,10 +11,13 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"dotenv": "^17.2.3",
"ejs": "^4.0.1", "ejs": "^4.0.1",
"express": "^5.2.1", "express": "^5.2.1",
"express-session": "^1.18.2", "express-session": "^1.18.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"jwks-rsa": "^3.2.0",
"nodemon": "^3.1.11" "nodemon": "^3.1.11"
} }
} }

View File

@@ -0,0 +1,138 @@
/* General Body Styles */
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
}
/* Notice Bar */
.notice-bar {
background-color: #eef6ff;
border-bottom: 1px solid #d1e0f0;
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.notice-bar p {
margin: 0;
}
/* CTA Button Styles */
.cta-button {
height: 44px;
padding: 0 24px;
border: none;
border-radius: 8px;
background-color: #A19FE7;
color: white;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
.cta-button:hover {
background-color: #583ac7;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* Header/Hero Section */
.hero {
text-align: center;
padding: 120px 24px 60px; /* Add padding top to account for fixed notice bar */
}
.hero h1 {
font-size: 40px;
font-weight: 800;
margin-bottom: 16px;
}
.hero .status-text {
font-size: 18px;
color: #555;
}
.hero .status-text.logged-out {
font-size: 16px;
color: #777;
}
/* Content Section */
.content-section {
padding: 48px 0;
}
.content-section h2 {
text-align: center;
font-size: 28px;
margin-bottom: 32px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.card {
background-color: white;
border: 1px solid #e9ecef;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
padding: 24px;
text-align: left;
}
.card img {
width: 100%;
border-radius: 8px;
margin-bottom: 16px;
}
.card h3 {
margin-top: 0;
font-size: 20px;
}
.login-prompt {
text-align: center;
padding: 48px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* Responsive Styles */
@media (max-width: 768px) {
.notice-bar {
flex-direction: column;
padding: 12px;
}
.notice-bar p {
margin-bottom: 8px;
}
.hero {
padding-top: 150px;
}
.card-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -4,7 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (ssoLoginButton) { if (ssoLoginButton) {
ssoLoginButton.addEventListener('click', () => { ssoLoginButton.addEventListener('click', () => {
// Open the SSO provider's login page in a popup // Open the SSO provider's login page in a popup
const ssoUrl = '/sso_popup.html'; // This is our simulated SSO provider const ssoUrl = 'https://sso.hmac.kr/'; // Real SSO provider URL
const popupWidth = 500; const popupWidth = 500;
const popupHeight = 600; const popupHeight = 600;
const left = (screen.width / 2) - (popupWidth / 2); const left = (screen.width / 2) - (popupWidth / 2);
@@ -18,10 +18,11 @@ document.addEventListener('DOMContentLoaded', () => {
// Listen for a message from the popup // Listen for a message from the popup
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
// IMPORTANT: In a real app, verify the origin of the message for security // IMPORTANT: Verify the origin of the message for security
// if (event.origin !== 'https://your-sso-provider.com') { if (event.origin !== 'https://sso.hmac.kr') {
// return; console.warn('Received message from untrusted origin:', event.origin);
// } return;
}
// Check if the message contains the expected data structure // Check if the message contains the expected data structure
if (event.data && event.data.type === 'LOGIN_SUCCESS' && event.data.token) { if (event.data && event.data.type === 'LOGIN_SUCCESS' && event.data.token) {

View File

@@ -2,46 +2,57 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SSO Login</title> <title>Baron SSO Login</title>
<link rel="stylesheet" href="/css/style.css">
<style> <style>
body { font-family: sans-serif; text-align: center; padding: 20px; } body {
button { padding: 10px 20px; font-size: 16px; cursor: pointer; } font-family: system-ui, sans-serif;
text-align: center;
padding: 40px 20px;
background-color: #f8f9fa;
}
h2 {
font-size: 24px;
margin-bottom: 12px;
}
</style> </style>
</head> </head>
<body> <body>
<h2>Simulated SSO Provider</h2> <h2>Baron SSO Provider</h2>
<p>Click the button below to simulate a successful login.</p> <p>아래 버튼을 클릭하면 로그인이 완료됩니다.</p>
<button id="confirm-login-btn">Confirm Login</button> <button id="confirm-login-btn" class="cta-button">Confirm Login</button>
<script> <script>
document.getElementById('confirm-login-btn').addEventListener('click', () => { document.getElementById('confirm-login-btn').addEventListener('click', () => {
// --- Create a dummy JWT for demonstration --- // --- This script now creates a dummy JWT with a dynamic issuer ---
// Header (no changes needed)
const header = { alg: 'HS256', typ: 'JWT' };
// Payload with a random 'sub' to simulate different users const header = {
const payload = { alg: "RS256", // Using RS256 as it's common for SSO
sub: `sso-user-${Math.random().toString(36).substring(2, 10)}`, typ: "JWT",
name: 'John Doe', kid: "simulated-key-id" // Key ID for JWKS lookup
iat: Math.floor(Date.now() / 1000)
}; };
// In a real JWT, the signature would be generated with a secret key. const payload = {
// For the demo, we only need the header and payload. iss: "https://sso.baron.com", // Simulated issuer
const dummyToken = [ sub: `baron-user-${Math.random().toString(36).substring(2, 10)}`,
btoa(JSON.stringify(header)), name: "Simulated User",
btoa(JSON.stringify(payload)), iat: Math.floor(Date.now() / 1000),
'dummy-signature' exp: Math.floor(Date.now() / 1000) + (60 * 60) // Expires in 1 hour
].join('.'); };
// --- End of dummy JWT creation ---
// In a real scenario, this token would be signed by the SSO provider's private key.
// We are sending an unsigned token for structure demonstration. The verification
// on the server side will fail if it tries to verify the signature,
// but the demo setup is focused on decoding and key fetching.
const dummyToken = btoa(JSON.stringify(header)) + '.' + btoa(JSON.stringify(payload)) + '.dummies_signature';
// Send the token back to the parent window that opened the popup
// In a real app, the targetOrigin should be the specific URL of your application
window.opener.postMessage({ window.opener.postMessage({
type: 'LOGIN_SUCCESS', type: 'LOGIN_SUCCESS',
token: dummyToken token: dummyToken
}, '*'); }, '*');
// Close the popup after sending the message
window.close();
}); });
</script> </script>
</body> </body>

View File

@@ -3,31 +3,72 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Express SSO Demo</title> <title>Express SSO Login Demo</title>
<style> <link rel="stylesheet" href="/css/style.css">
body { font-family: sans-serif; text-align: center; padding-top: 50px; }
.user-info { margin-bottom: 20px; }
#sso-login-btn, .logout-link {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
</head> </head>
<body> <body>
<h1>Welcome to the Express SSO Demo</h1> <!-- 1. Top Notice Bar -->
<div class="notice-bar">
<div class="user-info">
<% if (user) { %> <% if (user) { %>
<span>Welcome, <strong><%= user.username %></strong>!</span> <p>환영합니다, <strong><%= user.username %></strong>!</p>
<p><a href="/logout" class="logout-link">Logout</a></p> <a href="/logout" class="cta-button">Logout</a>
<% } else { %> <% } else { %>
<p>You are not logged in.</p> <p>SSO로 로그인하면 회원 전용 글을 확인할 수 있습니다.</p>
<button id="sso-login-btn">Login with SSO</button> <button id="sso-login-btn" class="cta-button">Baron SSO Login</button>
<% } %> <% } %>
</div> </div>
<div class="container">
<!-- 2. Hero/Header Area -->
<header class="hero">
<h1>SSO LOGIN DEMO</h1>
<% if (user) { %>
<p class="status-text">Welcome, user!</p>
<% } else { %>
<p class="status-text logged-out">You are not logged in.</p>
<% } %>
</header>
<!-- 3. Content Area -->
<main class="content-section">
<h2>Blog</h2>
<% if (user) { %>
<div class="card-grid">
<div class="card">
<h3>회원 전용 컨텐츠</h3>
<p>로그인한 사용자에게만 보이는 특별한 컨텐츠입니다.</p>
</div>
<div class="card">
<h3>로그인 버튼 디자인</h3>
<p>일관성 있는 CTA 버튼 디자인 가이드입니다.</p>
</div>
<div class="card">
<h3>SSO 핸들러 로직 분석</h3>
<p>JWT 토큰을 검증하고 세션을 처리하는 과정을 살펴봅니다.</p>
</div>
<div class="card">
<h3>보안 강화 방안</h3>
<p>애플리케이션의 보안을 강화하기 위한 몇 가지 방법입니다.</p>
</div>
<div class="card">
<h3>EJS 템플릿 엔진 활용</h3>
<p>동적 웹 페이지를 만들기 위한 EJS 사용법을 알아봅니다.</p>
</div>
<div class="card">
<h3>CSS 스타일 가이드</h3>
<p>UI의 일관성을 유지하기 위한 스타일 규칙입니다.</p>
</div>
</div>
<% } else { %>
<div class="login-prompt">
<h3>회원 전용 글은 로그인 후 열람 가능합니다.</h3>
<p>상단 버튼을 클릭해 Baron SSO로 로그인해주세요.</p>
</div>
<% } %>
</main>
</div>
<script src="/js/sso.js"></script> <script src="/js/sso.js"></script>
</body> </body>
</html> </html>