This repository has been archived on 2025-12-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
qmi-cloud/server/routes/qsProxy.js
Manuel Romero d5d7a128d2 OKTA login
2024-02-19 15:37:32 +01:00

270 lines
7.7 KiB
JavaScript

const express = require('express')
const router = express.Router()
const jsonwebtoken = require("jsonwebtoken");
const fs = require("fs");
const path = require("path");
const cookie = require("cookie");
const cookieParser = require("cookie-parser");
const axios = require("axios");
const { v4: uuidv4 } = require("uuid");
const ws = require("ws");
const WebSocketServer = ws.WebSocketServer;
const WebSocket = ws.WebSocket;
const passport = require('../passport-okta');
// This is the frontend application uri used for responding to requests.
//const frontendUri = "https://outstanding-desert-gatsby.glitch.me";
const privKey = fs.readFileSync(path.resolve(__dirname, '..', 'certs', 'privateKey.pem'));
const TENANT_DOMAIN = process.env["TENANT_DOMAIN"] || "innovation.us.qlikcloud.com";
const JWT_KEYID = process.env["JWT_KEYID"] || "8889be0d-6cf0-44eb-9fef-24bbc83712c5";
const JWT_ISSUER = process.env["JWT_ISSUER"] || TENANT_DOMAIN;
const JWT_USER_GROUPS = ["AnonJWTGroup"];
console.log("QSProxy# - TENANT_DOMAIN", TENANT_DOMAIN);
const auth = {
generateToken: function (user) {
// kid and issuer have to match with the IDP config and the audience has to be qlik.api/jwt-login-session
const signingOptions = {
keyid: JWT_KEYID,
algorithm: "RS256",
issuer: JWT_ISSUER,
expiresIn: "60m",
notBefore: "0s",
audience: "qlik.api/login/jwt-session",
};
// These are the claims that will be accepted and mapped anything else will be ignored. sub, subtype and name are mandatory.
const uuid = uuidv4();
userEmail = user.mail;
const payload = {
jti: uuid,
sub: `az/${user.sub}`,
subType: "user",
name: user.displayName,
email: userEmail,
email_verified: true,
groups: JWT_USER_GROUPS,
};
const token = jsonwebtoken.sign(payload, privKey, signingOptions);
return token;
},
getQlikSessionCookie: async function (token) {
try {
const resp = await axios(`https://${TENANT_DOMAIN}/login/jwt-session`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (resp.status === 200) {
console.log("QSProxy# - login/jwt-session", resp.headers['set-cookie'])
return resp.headers['set-cookie']
.map((e) => {
return e.split(";")[0];
})
.join(";");
}
return "";
} catch (e) {
console.log("QSProxy# - ERRRR", e);
return "";
}
},
};
const getStoreData = function (sidParsed) {
const store = passport.sessionStore;
return new Promise((resolve) => {
store.get(sidParsed, (err, session) => {
if (err) {
throw err;
}
return resolve(session);
});
});
};
router.get("/*.js", async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
// fetch resource from qlik using a redirect instead of proxy
// This endpoint is necessary when your web application uses the capability API.
router.get("/resources/*", async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
router.get('/assets/*', async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
// Issues the necessary pre-flight request to make sure the browser
// knows how to work with the web application.
router.options("/*", async (req, res) => {
setCors(res);
res.status(200).end();
});
function setCors(res) {
//res.set("Access-Control-Allow-Origin", "http://localhost:3000");
//res.set("Access-Control-Allow-Methods", "GET, OPTIONS");
//res.set("Access-Control-Allow-Headers", "Content-Type, x-proxy-session-id");
//res.set("Access-Control-Allow-Credentials", "true");
}
async function newSession(req){
console.log("QSProxy# - New cookie request for user", req.user.mail);
const jwtToken = auth.generateToken(req.user);
const qlikSession = await auth.getQlikSessionCookie(jwtToken);
return encodeURIComponent(qlikSession);
}
// Intercepts a request to one of Qlik's REST APIs and proxies the request to
// Qlik Cloud.
router.get("/api/v1/*", passport.ensureAuthenticated, async (req, res) => {
setCors(res);
var session = req.session;
var reqHeaders = {};
try {
if (session && session.id && session.qlikSession) {
console.log("QSProxy# - COOKIE FROM session", session.qlikSession);
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
} else {
const newS = await newSession(req);
session.qlikSession = newS;
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
}
try {
console.log("QSProxy# - qlikSession", reqHeaders.cookie);
const { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
headers: reqHeaders,
});
res.status(status).end(data);
} catch (e2) {
console.log("QSProxy# Error: QlikSession expired, requesting a new one.");
let newS = await newSession(req);
session.qlikSession = newS;
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
let { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
headers: reqHeaders,
});
res.status(status).end(data);
}
} catch (e) {
res.status(500).end("Error obtaining qlik session");
}
});
router.get('/qlik-embed-iframe/*', async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
function init (server) {
// Websocket section for intercepting websocket requests from the
// frontend application. When the front end application communicates
// communicates with the backend using websockets, this set of
// functions will be invoked.
const wss = new WebSocketServer({ server });
wss.on("connection", async function connection(ws, req) {
let isOpened = false;
// WebSockets do not have access to session information.
// To get the session you need to parse the 1st-party cookie.
// This will give you access to the Qlik Cloud cookie in order
// to proxy requests.
const cookieString = req.headers.cookie;
let qlikCookie = "";
if (cookieString) {
const cookieParsed = cookie.parse(cookieString);
const appCookie = cookieParsed["connect.sid"];
if (appCookie) {
const sidParsed = cookieParser.signedCookie(appCookie, "secret");
var session = await getStoreData(sidParsed);
qlikCookie = decodeURIComponent(session.qlikSession);
}
}
const appId = req.url.match("/app/(.*)\\?")[1];
if (!qlikCookie){
console.log("QSProxy# - Error in Websocket: NO qlikCookie!");
return;
}
const matchCookie = qlikCookie.match("_csrfToken=(.*);");
if (!matchCookie) {
console.log("QSProxy# - Error in Websocket: cant find _csrfToken= in qlikCookie");
return;
}
const csrfToken = matchCookie[1];
var wsConnUrl = `wss://${TENANT_DOMAIN}/app/${appId}?qlik-csrf-token=${csrfToken}`;
const qlikWebSocket = new WebSocket(
wsConnUrl,
{
headers: {
cookie: qlikCookie,
},
}
);
qlikWebSocket.on("error", console.error);
const openPromise = new Promise((resolve) => {
qlikWebSocket.on("open", function open() {
resolve();
});
});
ws.on("message", async function message(data) {
if (!isOpened) {
await openPromise;
isOpened = true;
}
qlikWebSocket.send(data.toString());
});
qlikWebSocket.on("message", function message(data) {
ws.send(data.toString());
});
});
}
module.exports.router = router;
module.exports.init = init;