From fe7d049b8a899e408c97267d7b1b58d334a89353 Mon Sep 17 00:00:00 2001 From: Rory Shanks <6383578+rorylshanks@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:03:06 +0100 Subject: [PATCH] Added tokenclaims idp_adapter (#17) --- Makefile | 1 + app.js | 4 +-- build.sh | 4 +-- example-config.yaml | 1 + lib/authz.js | 49 ++----------------------- lib/idp.js | 64 +++++++++++++++++++++++++++++++++ lib/idp_adapters/tokenclaims.js | 50 ++++++++++++++++++++++++++ lib/sso.js | 8 +++-- util/utils.js | 10 +++++- 9 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 lib/idp.js create mode 100644 lib/idp_adapters/tokenclaims.js diff --git a/Makefile b/Makefile index 0e4446d..ed50600 100644 --- a/Makefile +++ b/Makefile @@ -5,4 +5,5 @@ test-e2e: docker compose -f docker-compose-test.yaml up -d @echo "Waiting for start..." && sleep 5 @cd ./test/e2e && PUPPETEER_DISABLE_HEADLESS_WARNING=true npm run test || (docker compose -f ../../docker-compose-test.yaml logs vftest && exit 1) +# @docker compose -f docker-compose-test.yaml logs | grep -i error docker compose -f docker-compose-test.yaml down \ No newline at end of file diff --git a/app.js b/app.js index 3afde3f..e892fbe 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,12 @@ import { reloadConfig } from './util/config.js'; -import authz from './lib/authz.js'; +import idp from './lib/idp.js'; import log from './util/logging.js' async function main() { log.info("Starting Verflow Server") await reloadConfig() - await authz.scheduleUpdate() + await idp.scheduleUpdate() } main() diff --git a/build.sh b/build.sh index 2aa3922..9a8866d 100755 --- a/build.sh +++ b/build.sh @@ -1,10 +1,8 @@ #!/bin/bash -docker buildx create --use --name multi --platform linux/arm64,linux/amd64 docker buildx build \ --platform linux/arm64,linux/amd64 \ - -t rorylshanks/veriflow:latest \ + -t rorylshanks/veriflow:debug \ --push \ -f Dockerfile \ . -docker buildx rm multi diff --git a/example-config.yaml b/example-config.yaml index 4222bed..237c6e2 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -6,6 +6,7 @@ cookie_secret: "ThisIsAFakeCookieSecret" cookie_settings: sameSite: "none" secure: true + maxAge: 86400 redis_connection_string: redis://127.0.0.1:6379 idp_client_id: 00000000-1111-2222-3333-444444444444 idp_client_secret: "FAKEFAKEFAKEaingaigaeW9eic4ok3oojietheeFAKEFAKEFAKE" diff --git a/lib/authz.js b/lib/authz.js index ea45a41..9b67035 100644 --- a/lib/authz.js +++ b/lib/authz.js @@ -1,59 +1,15 @@ -import redisHelper from "../util/redis.js" -import Bossbat from 'bossbat'; import log from '../util/logging.js' -import { getConfig } from '../util/config.js' +import idp from './idp.js' import { createJWT } from '../util/jwt.js'; -import timestring from 'timestring'; import Cache from 'cache'; import fs from 'fs/promises' let requestHeaderMapCache = new Cache(60 * 1000); -const redisClient = redisHelper.getClient() - -const idpUpdater = new Bossbat({ - connection: redisHelper.getRedisConfig(), - prefix: 'bossbat:', - ttl: timestring(getConfig().idp_refresh_directory_interval) * 1000 -}); - -var currentConfig = getConfig() -let importedAdapter = await import(`./idp_adapters/${currentConfig.idp_provider}.js`) -let adapter = importedAdapter.default - -async function update() { - try { - var startDate = Date.now() - await adapter.runUpdate() - var endDate = Date.now() - var duration = (endDate - startDate) / 1000 - log.info(`Updated users from IDP in ${duration} seconds`) - } catch (error) { - log.error({error, details: error.message}) - } -} - -async function scheduleUpdate() { - let config = getConfig() - if (config.refresh_idp_at_start) { - update() - } - idpUpdater.hire('update-idp', { - every: getConfig().idp_refresh_directory_interval, - work: async () => { - try { - await update() - } catch (error) { - log.error({ message: "Failed up update users and groups from IDP", error }) - } - }, - }); -} - async function authZRequest(req, res, route) { var requestUrl = new URL(`${req.get("X-Forwarded-Proto")}://${req.get("X-Forwarded-Host")}${req.get("X-Forwarded-Path") || ""}`) var userId = req.session.userId - var user = await adapter.getUserById(userId) + var user = await idp.getUserById(userId) if (!user) { log.info({ "action": "userDoesNotExistInIdp", "user": userId, context: { url: requestUrl } }) return false @@ -158,6 +114,5 @@ async function getRequestHeaderMapConfig(user, route) { } export default { - scheduleUpdate, authZRequest } \ No newline at end of file diff --git a/lib/idp.js b/lib/idp.js new file mode 100644 index 0000000..562b8d8 --- /dev/null +++ b/lib/idp.js @@ -0,0 +1,64 @@ +import redisHelper from "../util/redis.js" +import Bossbat from 'bossbat'; +import log from '../util/logging.js' +import { getConfig } from '../util/config.js' +import timestring from 'timestring'; + +const idpUpdater = new Bossbat({ + connection: redisHelper.getRedisConfig(), + prefix: 'bossbat:', + ttl: timestring(getConfig().idp_refresh_directory_interval) * 1000 +}); + +var currentConfig = getConfig() +let importedAdapter = await import(`./idp_adapters/${currentConfig.idp_provider}.js`) +let adapter = importedAdapter.default + +async function update() { + try { + var startDate = Date.now() + await adapter.runUpdate() + var endDate = Date.now() + var duration = (endDate - startDate) / 1000 + log.info(`Updated users from IDP in ${duration} seconds`) + } catch (error) { + log.error({error, details: error.message}) + } +} + +async function scheduleUpdate() { + let config = getConfig() + if (config.refresh_idp_at_start) { + update() + } + idpUpdater.hire('update-idp', { + every: getConfig().idp_refresh_directory_interval, + work: async () => { + try { + await update() + } catch (error) { + log.error({ message: "Failed up update users and groups from IDP", error }) + } + }, + }); +} + +async function getUserById(userId) { + var user = await adapter.getUserById(userId) + return user +} + +async function addNewUserFromClaims(userClaims) { + if (!adapter.addNewUserFromClaims) { + log.debug({ message: `Adapter ${currentConfig.idp_provider} does not support adding new users via claims, returning`, context: { claims: userClaims } }) + return + } + log.debug({ message: "Attempting to add new user from claims", context: { claims: userClaims } }) + await adapter.addNewUserFromClaims(userClaims) +} + +export default { + getUserById, + scheduleUpdate, + addNewUserFromClaims +} \ No newline at end of file diff --git a/lib/idp_adapters/tokenclaims.js b/lib/idp_adapters/tokenclaims.js new file mode 100644 index 0000000..ca879f6 --- /dev/null +++ b/lib/idp_adapters/tokenclaims.js @@ -0,0 +1,50 @@ +import log from '../../util/logging.js' +import Cache from 'cache'; +import redisHelper from '../../util/redis.js' +import { getConfig } from '../../util/config.js'; + +const redisClient = redisHelper.getClient() + +let idpRedisResponse = new Cache(60 * 1000); + +async function runUpdate() { + return true +} + +async function getUserById(id) { + var idpResponse = idpRedisResponse.get(`veriflow:users:${id}`) + if (idpResponse) { + log.trace(`Returning IDP user ${id} from cache`) + return idpResponse + } else { + try { + log.debug("Cache miss, returning results from Redis") + var idpResponse = JSON.parse(await redisClient.get(`veriflow:users:${id}`)) + idpRedisResponse.put(`veriflow:users:${id}`, idpResponse) + return idpResponse + } catch (error) { + log.error({ message: "Error getting user by ID", error: error.message }) + return null + } + } +} + +async function addNewUserFromClaims(claims) { + var currentConfig = getConfig() + var userId = claims[currentConfig.idp_provider_user_id_claim] + + var userData = { + id: userId, + mail: claims.email, + ...claims + }; + + await redisClient.set(`veriflow:users:${userId}`, JSON.stringify(userData)) + await redisClient.expire(`veriflow:users:${userId}`, 87000); // expire in 24 hours +} + +export default { + runUpdate, + getUserById, + addNewUserFromClaims +}; \ No newline at end of file diff --git a/lib/sso.js b/lib/sso.js index e690b52..43ad5df 100644 --- a/lib/sso.js +++ b/lib/sso.js @@ -8,6 +8,7 @@ import dynamicBackend from './dynamic-backend.js' import { checkAuthHeader } from './token-auth.js' import crypto from 'crypto' import errorpages from '../util/errorpage.js' +import idp from './idp.js' function addParamsToUrl(baseUrl, params) { const url = new URL(baseUrl); @@ -262,15 +263,18 @@ async function verifySsoCallback(req, res) { // var userInfo = await oauth_client.userinfo(callbackInfo.access_token) - var userIdClaim = callbackInfo.claims()[currentConfig.idp_provider_user_id_claim] + var userClaims = callbackInfo.claims() + var userIdClaim = userClaims[currentConfig.idp_provider_user_id_claim] if (!userIdClaim) { - log.warn({ error: "User does not have a userId included in the ID token claims. Check setting idp_provider_user_id_claim", claims: callbackInfo.claims() }) + log.warn({ error: "User does not have a userId included in the ID token claims. Check setting idp_provider_user_id_claim", claims: userClaims }) var html = await errorpages.renderErrorPage(500, "ERR_NO_USERID_IN_TOKEN") res.status(500).send(html) return } + await idp.addNewUserFromClaims(userClaims) + req.session.loggedin = true; req.session.userId = userIdClaim; diff --git a/util/utils.js b/util/utils.js index 7750a4d..a80669f 100644 --- a/util/utils.js +++ b/util/utils.js @@ -11,6 +11,14 @@ function urlToCaddyUpstream(url) { return `${toURL.hostname}:${toPort}` } +function convertHeaderCase(str) { + return str + .split('-') // Split the string into an array of words by hyphen + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // Capitalize the first letter of each word and make the rest lowercase + .join('-'); // Rejoin the words with hyphens +} + export default { - urlToCaddyUpstream + urlToCaddyUpstream, + convertHeaderCase } \ No newline at end of file