// Advantech SSO + Node.js / Express — npm i express express-session (Node 18+) // // SSO_CLIENT_ID=svc_xxxxxxxx SSO_CLIENT_SECRET=xxxxxxxx \ // APP_URL=https://myapp.advantech.world SESSION_SECRET=long-random-string \ // node express_app.mjs (put login.html / index.html in ./public) // // Register APP_URL/auth/callback as the Redirect URI of the service. // express-session keeps sessions in memory: use a store (Redis, DB ...) in production. import crypto from "node:crypto"; import express from "express"; import session from "express-session"; const SSO = "https://ae-sso.advantech.com"; const { SSO_CLIENT_ID: CLIENT_ID, SSO_CLIENT_SECRET: CLIENT_SECRET } = process.env; const APP_URL = process.env.APP_URL.replace(/\/$/, ""); const REDIRECT_URI = `${APP_URL}/auth/callback`; const app = express(); app.set("trust proxy", 1); app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: APP_URL.startsWith("https://"), sameSite: "lax", maxAge: 8 * 3600e3 }, })); const safeNext = (v) => (typeof v === "string" && v.startsWith("/") && !v.startsWith("//") ? v : "/"); // ---------- 1. sign in ---------- // /auth/login?silent=1 adds prompt=none: if the user already signed in to ANY // service, the SSO returns straight away (no page, no click); otherwise it comes // back with error=login_required and we show login.html. app.get("/auth/login", (req, res) => { const state = crypto.randomBytes(16).toString("hex"); // remember several pending sign-ins (two tabs, a double click ...), newest 5 const pending = Object.entries(req.session.pending || {}).slice(-4); req.session.pending = Object.fromEntries([...pending, [state, safeNext(req.query.next)]]); const q = new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: "code", state }); if (req.query.silent === "1") q.set("prompt", "none"); res.redirect(`${SSO}/authorize?${q}`); }); app.get("/auth/callback", async (req, res) => { const pending = req.session.pending || {}; const next = Object.hasOwn(pending, String(req.query.state)) ? pending[String(req.query.state)] : null; delete pending[String(req.query.state)]; if (next === null) return res.redirect("/login.html?error=state"); // unknown / reused state if (req.query.error) { // login_required (silent try) / access_denied const q = new URLSearchParams({ next }); if (req.query.error !== "login_required") q.set("error", String(req.query.error)); return res.redirect(`/login.html?${q}`); } const r = await fetch(`${SSO}/token`, { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: String(req.query.code || ""), redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }), }); if (!r.ok) return res.redirect("/login.html?error=token"); const { user } = await r.json(); req.session.regenerate(() => { // new session id after sign-in req.session.user = user; // sub, email, name, department, picture ... res.redirect(next); }); }); app.get("/auth/logout", (req, res) => { req.session.destroy(() => { res.redirect(`${SSO}/logout?` + new URLSearchParams({ redirect_uri: `${APP_URL}/` })); }); }); // ---------- 2. is the user signed in? ---------- function requireLogin(req, res, next) { if (req.session.user) return next(); if (req.path.startsWith("/api/")) return res.status(401).json({ error: "not signed in" }); // try single sign-on first; login.html is shown only if that fails res.redirect("/auth/login?" + new URLSearchParams({ silent: "1", next: req.originalUrl })); } // ---------- 3. user data (used by advantech-sso.js) ---------- app.get("/auth/me", (req, res) => { if (!req.session.user) return res.status(401).json({ error: "not signed in" }); res.json(req.session.user); }); // ---------- your app ---------- app.get("/", requireLogin, (req, res) => res.sendFile("index.html", { root: "public" })); app.get("/api/hello", requireLogin, (req, res) => { res.json({ message: `Hello ${req.session.user.name}`, department: req.session.user.department }); }); app.use(express.static("public")); // login.html etc. app.listen(3000, () => console.log("http://localhost:3000"));