arcadia-suite-sv/server/plus/sso.ts

134 lines
4.1 KiB
TypeScript

import { Router, Request, Response } from "express";
import crypto from "crypto";
const router = Router();
function getSsoSecret(): string {
const secret = process.env.SSO_SECRET;
if (!secret || secret.length < 32) {
throw new Error("SSO_SECRET must be set with at least 32 characters");
}
return secret;
}
function generateSsoToken(user: any): { token: string; signature: string; timestamp: number } {
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(16).toString("hex");
const tokenData = {
sub: user.id,
username: user.username,
email: user.email || `${user.username}@arcadia.local`,
name: user.name || user.username,
role: user.role,
iss: "arcadia-suite",
aud: "arcadia-plus",
iat: timestamp,
exp: timestamp + 300,
nonce,
};
const token = Buffer.from(JSON.stringify(tokenData)).toString("base64url");
const signature = crypto
.createHmac("sha256", getSsoSecret())
.update(token + timestamp)
.digest("hex");
return { token, signature, timestamp };
}
router.get("/redirect", (req: Request, res: Response) => {
if (!req.isAuthenticated() || !req.user) {
return res.redirect("/auth");
}
const { token, signature, timestamp } = generateSsoToken(req.user);
// Check for stored redirect destination
const plusRedirect = (req.session as any).plusRedirect;
if (plusRedirect) {
delete (req.session as any).plusRedirect;
}
// Add redirect_to parameter if we have a destination
const redirectParam = plusRedirect ? `&redirect_to=${encodeURIComponent(plusRedirect)}` : '';
const ssoUrl = `/plus/sso/login?token=${encodeURIComponent(token)}&sig=${signature}&ts=${timestamp}${redirectParam}`;
res.redirect(ssoUrl);
});
router.get("/token", (req: Request, res: Response) => {
if (!req.isAuthenticated() || !req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
const { token, signature, timestamp } = generateSsoToken(req.user);
res.json({
token,
signature,
timestamp,
redirectUrl: `/plus/sso/login?token=${encodeURIComponent(token)}&sig=${signature}&ts=${timestamp}`,
});
});
// Direct auto-login via fetch to Laravel and proxy response
router.get("/auto-login", async (req: Request, res: Response) => {
try {
const PLUS_HOST = process.env.PLUS_HOST || "localhost";
const PLUS_PORT = process.env.PLUS_PORT || "8080";
// Forward request to Laravel auto-login
const response = await fetch(`http://${PLUS_HOST}:${PLUS_PORT}/auto-login`, {
redirect: "manual",
headers: {
"Accept": "text/html,application/xhtml+xml",
},
});
// Get Laravel session cookies
const setCookies = response.headers.getSetCookie();
// Forward cookies to client
for (const cookie of setCookies) {
res.setHeader("Set-Cookie", cookie);
}
// If Laravel redirects, follow manually and proxy the result
const location = response.headers.get("location");
if (location) {
// Follow the redirect
const homeResponse = await fetch(`http://${PLUS_HOST}:${PLUS_PORT}${location.replace(/^http:\/\/[^/]+/, '')}`, {
headers: {
"Cookie": setCookies.join("; "),
"Accept": "text/html,application/xhtml+xml",
},
});
let html = await homeResponse.text();
// Rewrite URLs to use /plus prefix
html = html.replace(/href="\//g, 'href="/plus/');
html = html.replace(/src="\//g, 'src="/plus/');
html = html.replace(/action="\//g, 'action="/plus/');
html = html.replace(/url\('\//g, "url('/plus/");
html = html.replace(/url\("\//g, 'url("/plus/');
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.send(html);
} else {
const html = await response.text();
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.send(html);
}
} catch (error: any) {
console.error("[Plus SSO] Auto-login error:", error);
res.status(502).json({
error: "Failed to connect to Arcádia Plus",
message: error.message
});
}
});
export default router;