First commit

This commit is contained in:
kyy
2026-01-15 14:12:41 +09:00
commit b28238cfd3
15 changed files with 1949 additions and 0 deletions

5
sso-demo/.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
# Ignore dependencies, as they will be installed inside the container
node_modules
# Ignore npm debug logs
npm-debug.log*

35
sso-demo/app.js Normal file
View File

@@ -0,0 +1,35 @@
const express = require('express');
const session = require('express-session');
const path = require('path');
const ssoHandler = require('./middleware/ssoHandler');
const indexRouter = require('./routes/index');
const app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Session middleware setup
app.use(session({
secret: 'a-very-secret-key-that-should-be-in-env-vars', // In production, use environment variables
resave: false,
saveUninitialized: true,
cookie: { secure: false } // In production, set secure: true if using HTTPS
}));
// Static files setup
app.use(express.static(path.join(__dirname, 'public')));
// SSO token handler middleware
app.use(ssoHandler);
// Routes
app.use('/', indexRouter);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
module.exports = app;

View File

@@ -0,0 +1,51 @@
const jwt = require('jsonwebtoken');
const User = require('../models/user');
async function ssoHandler(req, res, next) {
// 1. Check if the token is in the query parameters
const { token } = req.query;
if (token) {
try {
// 2. Decode the JWT to get the payload
// 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 (!decoded || !decoded.sub) {
return res.status(400).send('Invalid token: "sub" claim is missing.');
}
// 3. Find user by 'sub' claim
let user = await User.findBySsoSub(decoded.sub);
// 4. If user doesn't exist, create a new one (auto-registration)
if (!user) {
user = await User.createUser({ sso_sub: decoded.sub });
}
// 5. Save user information in the session
req.session.userId = user.id;
// 6. Redirect to the same URL without the token parameter
const redirectUrl = req.path;
return res.redirect(redirectUrl);
} catch (error) {
console.error('SSO handling failed:', error);
return res.status(500).send('An error occurred during SSO processing.');
}
}
// Attach user to request if session exists
if (req.session.userId) {
res.locals.user = await User.findById(req.session.userId);
} else {
res.locals.user = null;
}
return next();
}
module.exports = ssoHandler;

46
sso-demo/models/user.js Normal file
View File

@@ -0,0 +1,46 @@
// In-memory user store for demonstration purposes
// In a real application, this would be a database model (e.g., Sequelize, Mongoose)
const users = [];
let nextId = 1;
/**
* Finds a user by their SSO subject ID.
* @param {string} ssoSub - The SSO subject ID.
* @returns {Promise<object|null>} The user object or null if not found.
*/
async function findBySsoSub(ssoSub) {
return users.find(user => user.sso_sub === ssoSub) || null;
}
/**
* Finds a user by their internal ID.
* @param {number} id - The user's internal ID.
* @returns {Promise<object|null>} The user object or null if not found.
*/
async function findById(id) {
return users.find(user => user.id === id) || null;
}
/**
* Creates a new user.
* @param {object} userData - The user data.
* @param {string} userData.sso_sub - The SSO subject ID.
* @returns {Promise<object>} The newly created user object.
*/
async function createUser({ sso_sub }) {
const newUser = {
id: nextId++,
sso_sub: sso_sub,
// In a real app, you might get a username/email from the JWT or prompt the user
username: `user_${sso_sub.substring(0, 5)}`
};
users.push(newUser);
return newUser;
}
module.exports = {
findBySsoSub,
findById,
createUser
};

1415
sso-demo/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
sso-demo/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "sso-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ejs": "^4.0.1",
"express": "^5.2.1",
"express-session": "^1.18.2",
"jsonwebtoken": "^9.0.3",
"nodemon": "^3.1.11"
}
}

37
sso-demo/public/js/sso.js Normal file
View File

@@ -0,0 +1,37 @@
document.addEventListener('DOMContentLoaded', () => {
const ssoLoginButton = document.getElementById('sso-login-btn');
if (ssoLoginButton) {
ssoLoginButton.addEventListener('click', () => {
// Open the SSO provider's login page in a popup
const ssoUrl = '/sso_popup.html'; // This is our simulated SSO provider
const popupWidth = 500;
const popupHeight = 600;
const left = (screen.width / 2) - (popupWidth / 2);
const top = (screen.height / 2) - (popupHeight / 2);
const popup = window.open(
ssoUrl,
'ssoLogin',
`width=${popupWidth},height=${popupHeight},top=${top},left=${left}`
);
// Listen for a message from the popup
window.addEventListener('message', (event) => {
// IMPORTANT: In a real app, verify the origin of the message for security
// if (event.origin !== 'https://your-sso-provider.com') {
// return;
// }
// Check if the message contains the expected data structure
if (event.data && event.data.type === 'LOGIN_SUCCESS' && event.data.token) {
popup.close();
// Reload the page with the token in the query string
// This will be handled by our backend ssoHandler middleware
window.location.search = `?token=${event.data.token}`;
}
}, { once: true }); // Use 'once' to automatically remove the listener after it's called
});
}
});

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSO Login</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 20px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
</style>
</head>
<body>
<h2>Simulated SSO Provider</h2>
<p>Click the button below to simulate a successful login.</p>
<button id="confirm-login-btn">Confirm Login</button>
<script>
document.getElementById('confirm-login-btn').addEventListener('click', () => {
// --- Create a dummy JWT for demonstration ---
// Header (no changes needed)
const header = { alg: 'HS256', typ: 'JWT' };
// Payload with a random 'sub' to simulate different users
const payload = {
sub: `sso-user-${Math.random().toString(36).substring(2, 10)}`,
name: 'John Doe',
iat: Math.floor(Date.now() / 1000)
};
// In a real JWT, the signature would be generated with a secret key.
// For the demo, we only need the header and payload.
const dummyToken = [
btoa(JSON.stringify(header)),
btoa(JSON.stringify(payload)),
'dummy-signature'
].join('.');
// --- End of dummy JWT creation ---
// 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({
type: 'LOGIN_SUCCESS',
token: dummyToken
}, '*');
});
</script>
</body>
</html>

20
sso-demo/routes/index.js Normal file
View File

@@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
// GET home page
router.get('/', (req, res, next) => {
// The ssoHandler middleware has already attached the user to res.locals
res.render('index', { user: res.locals.user });
});
// GET logout
router.get('/logout', (req, res, next) => {
req.session.destroy((err) => {
if (err) {
return next(err);
}
res.redirect('/');
});
});
module.exports = router;

33
sso-demo/views/index.ejs Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Express SSO Demo</title>
<style>
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>
<body>
<h1>Welcome to the Express SSO Demo</h1>
<div class="user-info">
<% if (user) { %>
<span>Welcome, <strong><%= user.username %></strong>!</span>
<p><a href="/logout" class="logout-link">Logout</a></p>
<% } else { %>
<p>You are not logged in.</p>
<button id="sso-login-btn">Login with SSO</button>
<% } %>
</div>
<script src="/js/sso.js"></script>
</body>
</html>