Skip to content

Commit

Permalink
Added support for redis_connection_string and refactored the getUserB…
Browse files Browse the repository at this point in the history
…yId to be idp_adapter specific (#16)
  • Loading branch information
rorylshanks authored Mar 3, 2024
1 parent 3f17689 commit 6487e02
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 175 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ An example configuration file can be found in `example-config.yaml`. A breakdown
- `cookie_settings`: Settings related to the session cookie set by Veriflow for each site.
- `sameSite`: Sets the sameSite attribute of the cookie. Default "Lax"
- `secure`: Sets whether the cookie should be secure. Default "false"
- `redis_host`: Hostname of the Redis database server.
- `redis_port`: Port of the Redis database server.
- `redis_connection_string`: Connection string for the redis server to connect to (e.g. redis://127.0.0.1:6379)
- `redis_host`: Hostname of the Redis database server. (deprecated, for backwards compatibility only)
- `redis_port`: Port of the Redis database server. (deprecated, for backwards compatibility only)
- `idp_client_id`: Client ID for communication with the Identity Provider (IdP).
- `idp_client_secret`: Secret key for authenticating with the Identity Provider (IdP).
- `idp_tenant_id`: Identifier for the specific tenant in the Identity Provider's system.
Expand All @@ -88,7 +89,7 @@ An example configuration file can be found in `example-config.yaml`. A breakdown
- `tls_skip_verify`: Whether to ignore upstream certificate errors. Default `false`
- `allow_public_unauthenticated_access`: Whether to allow public access to the route (note this will disable all Veriflow user-related functionality). Default `false`
- `claims_headers`: Headers to include in the JWT claims.
- `jwt_override_audience`: Sets the `aud` key of the JWT added to the header specified in claims_headers. By default it is the hostname of the upstream or
- `jwt_override_audience`: Sets the `aud` key of the JWT added to the header specified in claims_headers. By default it is the hostname of the `to`
- `allowed_groups`: Groups allowed access.
- `cors_allow_preflight`: Whether to allow preflight CORS requests (HTTP `OPTIONS` requests).
- `remove_request_headers`: Headers to remove from the request.
Expand Down
3 changes: 1 addition & 2 deletions example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ cookie_secret: "ThisIsAFakeCookieSecret"
cookie_settings:
sameSite: "none"
secure: true
redis_host: redis
redis_port: 6379
redis_connection_string: redis://127.0.0.1:6379
idp_client_id: 00000000-1111-2222-3333-444444444444
idp_client_secret: "FAKEFAKEFAKEaingaigaeW9eic4ok3oojietheeFAKEFAKEFAKE"
idp_tenant_id: 00000000-1111-2222-3333-444444444444
Expand Down
28 changes: 10 additions & 18 deletions lib/authz.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
import redis from 'redis';
import redisHelper from "../util/redis.js"
import Bossbat from 'bossbat';
import log from '../util/logging.js'
import { getConfig, getUserById } from '../util/config.js'
import { getConfig } from '../util/config.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 = redis.createClient({
url: 'redis://' + getConfig().redis_host + ":" + getConfig().redis_port
});

redisClient.connect()
const redisClient = redisHelper.getClient()

const idpUpdater = new Bossbat({
connection: { host: getConfig().redis_host, port: getConfig().redis_port },
connection: redisHelper.getRedisConfig(),
prefix: 'bossbat:',
ttl: timestring(getConfig().idp_refresh_directory_interval) * 1000
});

redisClient.on('error', (err) => {
log.error('Redis error: ', err);
});
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()
var currentConfig = getConfig()
let importedAdapter = await import(`./idp_adapters/${currentConfig.idp_provider}.js`)
let adapter = importedAdapter.default
var update = await adapter.runUpdate()
await redisClient.set('veriflow:users', JSON.stringify(update));
await adapter.runUpdate()
var endDate = Date.now()
var duration = (endDate - startDate) / 1000
log.info(`Updated users from IDP in ${duration} seconds`)
Expand Down Expand Up @@ -61,7 +53,7 @@ async function scheduleUpdate() {
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 getUserById(userId)
var user = await adapter.getUserById(userId)
if (!user) {
log.info({ "action": "userDoesNotExistInIdp", "user": userId, context: { url: requestUrl } })
return false
Expand Down Expand Up @@ -91,7 +83,7 @@ async function checkUserGroupMembership(user, groups) {
async function addRequestedHeaders(req, res, route, user, discoveredGroups) {
var proxyTo = {}
try {
proxyTo = new URL(route.to.url || route.to)
proxyTo = new URL(route.to.url || route.to.name || route.to)
} catch (error) {
log.warn({ message: "Unable to get audience from route", context: { error: error.message, stack: error.stack, route: route } })
}
Expand Down
11 changes: 2 additions & 9 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@ import session from 'express-session';
import ssoController from './sso.js';

import RedisStore from "connect-redis"
import { createClient } from "redis"
import redisHelper from "../util/redis.js"
import log from '../util/logging.js'
import { getConfig } from '../util/config.js';
import { pem2jwk } from 'pem-jwk';
import crypto from 'crypto';


// Initialize client.
let redisClient = createClient({
url: 'redis://' + getConfig().redis_host + ":" + getConfig().redis_port
})
redisClient.connect().catch(console.error)

// Initialize store.
let redisStore = new RedisStore({
client: redisClient,
client: redisHelper.getClient(),
prefix: "vfsession:",
})

Expand Down
42 changes: 39 additions & 3 deletions lib/idp_adapters/googleworkspace.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import axios from 'axios';
import fs from 'fs';
import log from '../../util/logging.js';
import {GoogleAuth} from 'google-auth-library';
import { getConfig } from '../../util/config.js';
import Cache from 'cache';
import redisHelper from '../../util/redis.js'

const redisClient = redisHelper.getClient()

let idpRedisResponse = new Cache(60 * 1000);

async function getAccessToken() {
const config = getConfig()
Expand Down Expand Up @@ -62,8 +67,39 @@ async function runUpdate() {
log.debug("Starting update of users and groups from Google Workspace");
const userData = await getUsersAndGroups();
fs.writeFileSync("output.json", JSON.stringify(userData, null, 2));
await redisClient.set('veriflow:users', JSON.stringify(update));
log.debug("Finished update of users and groups from Google Workspace");
return userData;
return true
}

async function getIdpConfig() {
var idpResponse = idpRedisResponse.get("veriflow:users")
if (idpResponse) {
log.trace("Returning IDP users from cache")
return idpResponse
} else {
try {
log.debug("Cache miss, returning results from Redis")
var idpResponse = JSON.parse(await redisClient.get('veriflow:users'))
idpRedisResponse.put("veriflow:users", idpResponse)
return idpResponse
} catch (error) {
log.error(error)
return null
}

}
}

async function getUserById(id) {
var config = await getIdpConfig()
if (!config) {
return null
}
return config[id]
}

export default { runUpdate };
export default {
runUpdate,
getUserById
};
20 changes: 18 additions & 2 deletions lib/idp_adapters/localfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@ import fs from 'fs/promises';
import { getConfig } from '../../util/config.js';
import log from '../../util/logging.js'

async function runUpdate() {
async function getLocalIdpConfig() {
const currentConfig = getConfig()
let localFile = currentConfig.idp_provider_localfile_location
let fileContents = await fs.readFile(localFile)
var result = JSON.parse(fileContents)
return result
}

async function runUpdate() {
var result = await getLocalIdpConfig()
log.debug(result)
return result
}

export default { runUpdate };
async function getUserById(id) {
var config = await getLocalIdpConfig()
if (!config) {
return null
}
return config[id]
}

export default {
runUpdate,
getUserById
};
42 changes: 40 additions & 2 deletions lib/idp_adapters/msgraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import pLimit from 'p-limit';
import { getConfig } from '../../util/config.js';
import log from '../../util/logging.js'
import fs from 'fs';
import Cache from 'cache';
import redisHelper from '../../util/redis.js'

const redisClient = redisHelper.getClient()

let idpRedisResponse = new Cache(60 * 1000);

async function getAccessToken(clientId, clientSecret, tenantId) {
const form = new FormData();
Expand Down Expand Up @@ -82,8 +88,40 @@ async function runUpdate() {
const currentConfig = getConfig()
var userGroups = await getUsersAndGroups(currentConfig.idp_client_id, currentConfig.idp_client_secret, currentConfig.idp_tenant_id)
fs.writeFileSync("output.json", JSON.stringify(userGroups))
await redisClient.set('veriflow:users', JSON.stringify(update));
log.debug("Finished update of users and groups from Microsoft Graph")
return userGroups
return true
}

export default { runUpdate };
async function getIdpConfig() {
var idpResponse = idpRedisResponse.get("veriflow:users")
if (idpResponse) {
log.trace("Returning IDP users from cache")
return idpResponse
} else {
try {
log.debug("Cache miss, returning results from Redis")
var idpResponse = JSON.parse(await redisClient.get('veriflow:users'))
idpRedisResponse.put("veriflow:users", idpResponse)
return idpResponse
} catch (error) {
log.error(error)
return null
}

}
}

async function getUserById(id) {
var config = await getIdpConfig()
if (!config) {
return null
}
return config[id]
}


export default {
runUpdate,
getUserById
};
1 change: 0 additions & 1 deletion lib/sso.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { URL, URLSearchParams } from 'url';
import log from '../util/logging.js'
import authz from './authz.js'
import dynamicBackend from './dynamic-backend.js'
import utils from '../util/utils.js'
import { checkAuthHeader } from './token-auth.js'
import crypto from 'crypto'
import errorpages from '../util/errorpage.js'
Expand Down
Loading

0 comments on commit 6487e02

Please sign in to comment.