From b8e40922e6ad42d5b668c42704b8e387d65f1f12 Mon Sep 17 00:00:00 2001 From: Galen Guyer Date: Tue, 29 Mar 2022 17:54:44 -0400 Subject: [PATCH 1/2] add route for impersonating users from association id --- .gitignore | 1 + impersonate.js | 83 ++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- pnpm-lock.yaml | 41 ++++++++++++++++++++ routes/memberProjects.js | 51 ++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 impersonate.js diff --git a/.gitignore b/.gitignore index 0d630a1..3f03d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ yarn-error.log npm-error.log /.vscode /.idea +.env diff --git a/impersonate.js b/impersonate.js new file mode 100644 index 0000000..0e6966c --- /dev/null +++ b/impersonate.js @@ -0,0 +1,83 @@ +const fetchPromise = import('node-fetch'); + +async function getSaToken() { + const fetch = (await fetchPromise).default; + + const resp = await fetch('https://sso.csh.rit.edu/auth/realms/master/protocol/openid-connect/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${Buffer.from(process.env.GK_SA_USERNAME + ":" + process.env.GK_SA_PASSWORD).toString('base64')}`, + }, + body: 'grant_type=client_credentials', + }); + const json = await resp.json(); + + return json['access_token']; +} + +async function getUidFromUsername(username, saToken) { + const fetch = (await fetchPromise).default; + + const resp = await fetch('https://sso.csh.rit.edu/auth/admin/realms/csh/users?username=' + username, { + headers: { + 'Authorization': `Bearer ${saToken}`, + }, + }); + const json = await resp.json(); + + return json[0]['id']; +} + +async function getImpersonationSession(userId, saToken) { + const fetch = (await fetchPromise).default; + + const resp = await fetch(`https://sso.csh.rit.edu/auth/admin/realms/csh/users/${userId}/impersonation`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${saToken}`, + } + }); + const headers = resp.headers; + const cookies = headers.get('set-cookie'); + + const identityRegex = /KEYCLOAK_IDENTITY=\S+/; + const sessionRegex = /KEYCLOAK_SESSION=\S+/; + + const identity = identityRegex.exec(cookies)[0].split('=')[1].replace(';', ''); + const session = sessionRegex.exec(cookies)[0].split('=')[1].replace(';', ''); + + return {identity, session}; +} + +async function getUserToken(identity, session) { + const fetch = (await fetchPromise).default; + + const resp = await fetch('https://sso.csh.rit.edu/auth/realms/csh/protocol/openid-connect/auth?client_id=gatekeeper&response_type=token&response_mode=fragment&redirect_uri=https%3A%2F%2Fgatekeeper.csh.rit.edu%2Fcallback', { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': `KEYCLOAK_SESSION=${session}; KEYCLOAK_IDENTITY=${identity};`, + }, + redirect: 'manual', + }); + + const headers = resp.headers; + const location = new URL(headers.get('location').replace('#', '?')); + + let accessToken = location.searchParams.get('access_token'); + + return accessToken; +} + +async function getImpersonationToken(userId) { + const saToken = await getSaToken(); + const uid = await getUidFromUsername(userId, saToken); + const {identity, session} = await getImpersonationSession(uid, saToken); + const accessToken = await getUserToken(identity, session); + return {uid, userId, accessToken}; +} + +module.exports = { + getImpersonationToken +}; diff --git a/package.json b/package.json index 82fff09..63dee2b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "ldapjs": "^2.3.1", "mongodb": "^3.6.9", "morgan": "^1.10.0", - "mqtt": "^4.2.6" + "mqtt": "^4.2.6", + "node-fetch": "^3.2.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93e2f59..2d0dc13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ specifiers: mongodb: ^3.6.9 morgan: ^1.10.0 mqtt: ^4.2.6 + node-fetch: ^3.2.3 dependencies: body-parser: 1.19.0 @@ -15,6 +16,7 @@ dependencies: mongodb: 3.6.9 morgan: 1.10.0 mqtt: 4.2.6 + node-fetch: 3.2.3 packages: @@ -179,6 +181,11 @@ packages: resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=} dev: false + /data-uri-to-buffer/4.0.0: + resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==} + engines: {node: '>= 12'} + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} dependencies: @@ -294,6 +301,14 @@ packages: engines: {'0': node >=0.6.0} dev: false + /fetch-blob/3.1.5: + resolution: {integrity: sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.2.0 + dev: false + /finalhandler/1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} @@ -307,6 +322,13 @@ packages: unpipe: 1.0.0 dev: false + /formdata-polyfill/4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.1.5 + dev: false + /forwarded/0.1.2: resolution: {integrity: sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=} engines: {node: '>= 0.6'} @@ -637,6 +659,20 @@ packages: engines: {node: '>= 0.6'} dev: false + /node-domexception/1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + + /node-fetch/3.2.3: + resolution: {integrity: sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.0 + fetch-blob: 3.1.5 + formdata-polyfill: 4.0.10 + dev: false + /on-finished/2.3.0: resolution: {integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=} engines: {node: '>= 0.8'} @@ -945,6 +981,11 @@ packages: extsprintf: 1.4.0 dev: false + /web-streams-polyfill/3.2.0: + resolution: {integrity: sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==} + engines: {node: '>= 8'} + dev: false + /wrappy/1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} dev: false diff --git a/routes/memberProjects.js b/routes/memberProjects.js index 1d4a223..21aba5e 100644 --- a/routes/memberProjects.js +++ b/routes/memberProjects.js @@ -1,5 +1,6 @@ const router = require("express").Router(); const ldap = require("../ldap"); +const impersonate = require("../impersonate"); function findUser(id) { return new Promise((resolve, reject) => { @@ -88,4 +89,54 @@ router.get("/by-key/:associationId", async (req, res) => { }); }); +router.get("/impersonate/:associationId", async (req, res) => { + const key = await req.ctx.db.collection("keys").findOne({ + [req.associationType]: {$eq: req.params.associationId}, + }); + + if (!key) { + res.status(404).json({message: "Not found"}); + return; + } + + const userDocument = await req.ctx.db.collection("users").findOne({ + id: {$eq: key.userId}, + disabled: {$ne: true}, + }); + if (!userDocument) { + res.status(404).json({message: "User not found or disabled"}); + return; + } + + + let user; + try { + user = await findUser(key.userId); + } catch (err) { + res.status(500).json({message: "Internal server error"}); + return; + } + + const response = {}; + for (const attribute of user.attributes) { + if (attribute.type == "jpegPhoto") { + response[attribute.type] = attribute._vals[0].toString("base64"); + } else { + const values = attribute._vals.map((value) => value.toString("utf8")); + if (ARRAYS.has(attribute.type)) { + response[attribute.type] = values; + } else { + if (values.length > 1) { + console.warn(`${attribute.type} has many values!!`); + } + response[attribute.type] = values.join(","); + } + } + } + + const uid = response["uid"]; + + res.json(await impersonate.getImpersonationToken(uid)); +}); + module.exports = router; From 8b66028a09e14b78df2f96340c7c41b3da025d41 Mon Sep 17 00:00:00 2001 From: Mary Strodl Date: Fri, 8 Apr 2022 08:33:58 -0400 Subject: [PATCH 2/2] Linter pass --- impersonate.js | 147 ++++++++++++++++++++++----------------- routes/memberProjects.js | 1 - 2 files changed, 82 insertions(+), 66 deletions(-) diff --git a/impersonate.js b/impersonate.js index 0e6966c..9a60fa8 100644 --- a/impersonate.js +++ b/impersonate.js @@ -1,83 +1,100 @@ -const fetchPromise = import('node-fetch'); +const fetchPromise = import("node-fetch"); async function getSaToken() { - const fetch = (await fetchPromise).default; - - const resp = await fetch('https://sso.csh.rit.edu/auth/realms/master/protocol/openid-connect/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(process.env.GK_SA_USERNAME + ":" + process.env.GK_SA_PASSWORD).toString('base64')}`, - }, - body: 'grant_type=client_credentials', - }); - const json = await resp.json(); - - return json['access_token']; + const fetch = (await fetchPromise).default; + + const resp = await fetch( + "https://sso.csh.rit.edu/auth/realms/master/protocol/openid-connect/token", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from( + process.env.GK_SA_USERNAME + ":" + process.env.GK_SA_PASSWORD + ).toString("base64")}`, + }, + body: "grant_type=client_credentials", + } + ); + const json = await resp.json(); + + return json["access_token"]; } async function getUidFromUsername(username, saToken) { - const fetch = (await fetchPromise).default; - - const resp = await fetch('https://sso.csh.rit.edu/auth/admin/realms/csh/users?username=' + username, { - headers: { - 'Authorization': `Bearer ${saToken}`, - }, - }); - const json = await resp.json(); - - return json[0]['id']; + const fetch = (await fetchPromise).default; + + const resp = await fetch( + "https://sso.csh.rit.edu/auth/admin/realms/csh/users?username=" + username, + { + headers: { + Authorization: `Bearer ${saToken}`, + }, + } + ); + const json = await resp.json(); + + return json[0]["id"]; } async function getImpersonationSession(userId, saToken) { - const fetch = (await fetchPromise).default; - - const resp = await fetch(`https://sso.csh.rit.edu/auth/admin/realms/csh/users/${userId}/impersonation`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${saToken}`, - } - }); - const headers = resp.headers; - const cookies = headers.get('set-cookie'); - - const identityRegex = /KEYCLOAK_IDENTITY=\S+/; - const sessionRegex = /KEYCLOAK_SESSION=\S+/; - - const identity = identityRegex.exec(cookies)[0].split('=')[1].replace(';', ''); - const session = sessionRegex.exec(cookies)[0].split('=')[1].replace(';', ''); - - return {identity, session}; + const fetch = (await fetchPromise).default; + + const resp = await fetch( + `https://sso.csh.rit.edu/auth/admin/realms/csh/users/${userId}/impersonation`, + { + method: "POST", + headers: { + Authorization: `Bearer ${saToken}`, + }, + } + ); + const headers = resp.headers; + const cookies = headers.get("set-cookie"); + + const identityRegex = /KEYCLOAK_IDENTITY=\S+/; + const sessionRegex = /KEYCLOAK_SESSION=\S+/; + + const identity = identityRegex + .exec(cookies)[0] + .split("=")[1] + .replace(";", ""); + const session = sessionRegex.exec(cookies)[0].split("=")[1].replace(";", ""); + + return {identity, session}; } async function getUserToken(identity, session) { - const fetch = (await fetchPromise).default; - - const resp = await fetch('https://sso.csh.rit.edu/auth/realms/csh/protocol/openid-connect/auth?client_id=gatekeeper&response_type=token&response_mode=fragment&redirect_uri=https%3A%2F%2Fgatekeeper.csh.rit.edu%2Fcallback', { - method: 'GET', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Cookie': `KEYCLOAK_SESSION=${session}; KEYCLOAK_IDENTITY=${identity};`, - }, - redirect: 'manual', - }); - - const headers = resp.headers; - const location = new URL(headers.get('location').replace('#', '?')); - - let accessToken = location.searchParams.get('access_token'); - - return accessToken; + const fetch = (await fetchPromise).default; + + const resp = await fetch( + "https://sso.csh.rit.edu/auth/realms/csh/protocol/openid-connect/auth?client_id=gatekeeper&response_type=token&response_mode=fragment&redirect_uri=https%3A%2F%2Fgatekeeper.csh.rit.edu%2Fcallback", + { + method: "GET", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Cookie: `KEYCLOAK_SESSION=${session}; KEYCLOAK_IDENTITY=${identity};`, + }, + redirect: "manual", + } + ); + + const headers = resp.headers; + const location = new URL(headers.get("location").replace("#", "?")); + + let accessToken = location.searchParams.get("access_token"); + + return accessToken; } async function getImpersonationToken(userId) { - const saToken = await getSaToken(); - const uid = await getUidFromUsername(userId, saToken); - const {identity, session} = await getImpersonationSession(uid, saToken); - const accessToken = await getUserToken(identity, session); - return {uid, userId, accessToken}; + const saToken = await getSaToken(); + const uid = await getUidFromUsername(userId, saToken); + const {identity, session} = await getImpersonationSession(uid, saToken); + const accessToken = await getUserToken(identity, session); + return {uid, userId, accessToken}; } module.exports = { - getImpersonationToken + getImpersonationToken, }; diff --git a/routes/memberProjects.js b/routes/memberProjects.js index 21aba5e..ec7bfc7 100644 --- a/routes/memberProjects.js +++ b/routes/memberProjects.js @@ -108,7 +108,6 @@ router.get("/impersonate/:associationId", async (req, res) => { return; } - let user; try { user = await findUser(key.userId);