diff --git a/database/layer/admin-session.js b/database/layer/admin-session.js index 5369376..ca60a53 100644 --- a/database/layer/admin-session.js +++ b/database/layer/admin-session.js @@ -1,46 +1,55 @@ const crypto = require('node:crypto') -const { PGPool } = require('./pg_pool') -const pool = new PGPool() - -function getAdminSessionView () { - return `SELECT p.key AS personakey, s.event_id, s.authorization_time, s.scope, s.idp_token - FROM usher.tenants t - JOIN usher.personas p ON p.tenantkey = t.key - JOIN usher.sessions s ON s.personakey = p.key` -} - -function getAdminTenantPersonaView () { - return `SELECT p.key as personakey - FROM usher.tenants t - JOIN usher.personas p ON p.tenantkey = t.key` -} +const { usherDb } = require('./knex') async function getSessionPersonaKey (subClaim, userContext = '', issClaim) { - const sql = getAdminSessionView() + ' WHERE sub_claim = $1 AND p.user_context = $2 AND iss_claim = $3' - const sessionKeyResult = await pool.query(sql, [subClaim, userContext, issClaim]) - return (sessionKeyResult.rows.length === 0 ? null : sessionKeyResult.rows[0].personakey) + const results = await usherDb('tenants as t') + .join('personas as p', 't.key', '=', 'p.tenantkey') + .join('sessions as s', 'p.key', '=', 's.personakey') + .select('p.key as personakey', 's.event_id', 's.authorization_time', 's.scope', 's.idp_token') + .where('sub_claim', subClaim) + .where('p.user_context', userContext) + .where('iss_claim', issClaim) + + return (results.length === 0 ? null : results[0].personakey) } async function getPersonaKey (subClaim, userContext = '', issClaim) { - const sql = getAdminTenantPersonaView() + ' WHERE sub_claim = $1 AND p.user_context = $2 AND iss_claim = $3' - const personaKeyResult = await pool.query(sql, [subClaim, userContext, issClaim]) - return personaKeyResult.rows.length === 0 ? null : personaKeyResult.rows[0].personakey + const results = await usherDb('tenants as t') + .join('personas as p', 't.key', '=', 'p.tenantkey') + .select('p.key as personakey') + .where('sub_claim', subClaim) + .where('p.user_context', userContext) + .where('iss_claim', issClaim) + + return results.length === 0 ? null : results[0].personakey } +/** + * Gets the most recent session record for the given User + * @param {string} subClaim + * @param {string} userContext + * @param {string} issClaim + * @returns An object representing the session record or null if no session exists + */ async function getSessionBySubIss (subClaim, userContext, issClaim) { const personaKey = await getSessionPersonaKey(subClaim, userContext, issClaim) if (!personaKey) { return null } - const sql = 'SELECT * FROM usher.sessions WHERE personakey = $1' - const sessionRowResult = await pool.query(sql, [personaKey]) - return sessionRowResult.rows[0] + const results = await usherDb('sessions').select().where('personakey', personaKey) + .orderBy('authorization_time', 'desc') + .first() + return results || null // force null return if no results instead of undefined } +/** + * Get a session record by a given session `event_id` + * @param {string} eventId The session event_id to look up + * @returns An object representing the session record + */ async function getSessionByEventId (eventId) { - const sql = 'SELECT * FROM usher.sessions WHERE event_id = $1' - const sessionRowResult = await pool.query(sql, [eventId]) - return sessionRowResult.rows.length === 0 ? null : sessionRowResult.rows[0] + const results = await usherDb('sessions').select().where('event_id', eventId) + return results.length === 0 ? null : results[0] } async function insertSessionBySubIss ( @@ -63,10 +72,16 @@ async function insertSessionBySubIss ( } async function insertSessionByPersonaKey (personakey, eventId, authorizationTime, idpExpirationTime, scope, idpToken) { - const sql = `INSERT INTO usher.sessions - (personakey, event_id, authorization_time, idp_expirationtime, scope, idp_token) - VALUES ($1, $2, $3, $4, $5, $6)` - return pool.query(sql, [personakey, eventId, authorizationTime, idpExpirationTime, scope, idpToken]) + const results = await usherDb('sessions').insert({ + personakey, + event_id: eventId, + authorization_time: authorizationTime, + idp_expirationtime: idpExpirationTime, + scope, + idp_token: idpToken + }) + .returning('*') + return results?.[0] } async function updateSessionBySubIss (subClaim, userContext, issClaim, authorizationTime, idpExpirationTime, scope, idpToken) { @@ -75,9 +90,16 @@ async function updateSessionBySubIss (subClaim, userContext, issClaim, authoriza throw new Error(`Session does not exist for persona (sub_claim=${subClaim} user_context = ${userContext} iss_claim=${issClaim})`) } - const sql = 'UPDATE usher.sessions SET authorization_time = $1, idp_expirationtime = $2, scope = $3, idp_token = $4 WHERE personakey = $5' - const results = await pool.query(sql, [authorizationTime, idpExpirationTime, scope, idpToken, personaKey]) - return results.rows + const [results] = await usherDb('sessions') + .where('personakey', personaKey) + .update({ + authorization_time: authorizationTime, + idp_expirationtime: idpExpirationTime, + scope, + idp_token: idpToken + }) + .returning('*') + return results } /** @@ -110,10 +132,9 @@ async function deleteSessionBySubIss (subClaim, userContext, issClaim) { return deleteReturn } -async function deleteSessionByPersonaKey (personakey) { - const sql = 'DELETE FROM usher.sessions WHERE personakey = $1' - const deleteReturn = await pool.query(sql, [personakey]) - if (deleteReturn.rowCount === 1) { +async function deleteSessionByPersonaKey (personaKey) { + const deleteResults = await usherDb('sessions').where('personakey', personaKey).del() + if (deleteResults === 1) { return 'Delete successful' } else { return 'Delete unsuccessful' diff --git a/database/layer/view-select-entities.js b/database/layer/view-select-entities.js index 8771ce7..53ab913 100644 --- a/database/layer/view-select-entities.js +++ b/database/layer/view-select-entities.js @@ -1,41 +1,37 @@ -const { PGPool } = require('./pg_pool') -const pool = new PGPool() +const { usherDb } = require('./knex') -function getTenantsView () { - return `SELECT t.name AS tenantname, t.iss_claim, t.jwks_uri - FROM usher.tenants t` -} -async function selectIssuerJWKS (issClaim = '*') { +/** + * + * @param {string} issClaim ISS Claim to look up tenant by + * @returns + */ +async function selectIssuerJWKS (issClaim) { try { - let sql = getTenantsView() + ' where 1=1' - const params = [] - let paramCount = 0 - if (issClaim !== '*') { - params.push(issClaim) - paramCount++ - sql += ' and iss_claim = $' + paramCount - } - sql += ' LIMIT 1' - const results = await pool.query(sql, params) - return results.rows + const results = await usherDb('tenants') + .select('name as tenantname', 'iss_claim', 'jwks_uri') + .where('iss_claim', issClaim) + .limit(1) + return results } catch (error) { throw error.message } } +/** + * Get a list of clients, if clientId is not provided, return all clients + * @param {*} clientId + * @returns + */ async function selectClients (clientId = '*') { try { - let sql = `SELECT c.client_id, c.name as clientname, c.description, c.secret - FROM usher.clients c where 1=1 ` - const params = [] - let paramCount = 0 - if (clientId !== '*') { - params.push(clientId) - paramCount++ - sql += ' and client_id = $' + paramCount - } - const results = await pool.query(sql, params) - return results.rows + const results = await usherDb('clients') + .select('client_id', 'name as clientname', 'description', 'secret') + .modify((queryBuilder) => { + if (clientId !== '*') { + queryBuilder.where('client_id', clientId) + } + }) + return results } catch (error) { throw error.message } diff --git a/database/package-lock.json b/database/package-lock.json index c5c517d..5251515 100644 --- a/database/package-lock.json +++ b/database/package-lock.json @@ -1,20 +1,20 @@ { "name": "@dmgt-tech/the-usher-server-database", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dmgt-tech/the-usher-server-database", - "version": "2.1.1", + "version": "2.1.2", "license": "MIT", "dependencies": { "dotenv": "16.4.5", "knex": "3.1.0", - "pg": "8.11.3" + "pg": "8.12.0" }, "devDependencies": { - "mocha": "^10.7.0" + "mocha": "^10.7.3" } }, "node_modules/ansi-colors": { @@ -111,14 +111,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/buffer-writer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", - "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", - "engines": { - "node": ">=4" - } - }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -675,9 +667,9 @@ } }, "node_modules/mocha": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.0.tgz", - "integrity": "sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", "dev": true, "dependencies": { "ansi-colors": "^4.1.3", @@ -791,11 +783,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/packet-reader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", - "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -811,15 +798,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/pg": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", - "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", "dependencies": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-connection-string": "^2.6.2", - "pg-pool": "^3.6.1", - "pg-protocol": "^1.6.0", + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -858,17 +843,17 @@ } }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -885,6 +870,11 @@ "node": ">=4" } }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, "node_modules/pgpass": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", diff --git a/database/package.json b/database/package.json index 54deb32..a7d8dab 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "@dmgt-tech/the-usher-server-database", - "version": "2.1.1", + "version": "2.1.2", "description": "Database layer for TheUsher", "scripts": { "test": "mocha --exit", @@ -24,9 +24,9 @@ "dependencies": { "dotenv": "16.4.5", "knex": "3.1.0", - "pg": "8.11.3" + "pg": "8.12.0" }, "devDependencies": { - "mocha": "^10.7.0" + "mocha": "^10.7.3" } } diff --git a/database/test/db-client.test.js b/database/test/db-client.test.js index b35adb1..574bcac 100644 --- a/database/test/db-client.test.js +++ b/database/test/db-client.test.js @@ -17,6 +17,11 @@ describe('Clients', function () { const CLIENT_ACTUAL1 = await viewSelectEntities.selectClients('test-client1') assert.strictEqual(JSON.stringify(CLIENT_ACTUAL1), JSON.stringify(CLIENT_EXPECTED1)) }) + + it('Should return multiple clients', async function () { + const results = await viewSelectEntities.selectClients() + assert(results.length >= 1, 'Expected more than one client') + }) }) describe('Test Client Roles requests', function () { diff --git a/database/test/db-insert.test.js b/database/test/db-insert.test.js index 042a9e6..af08c4f 100644 --- a/database/test/db-insert.test.js +++ b/database/test/db-insert.test.js @@ -125,12 +125,12 @@ describe('Insert Update and Delete tests', function () { describe('Test session insert', function () { const authorizationDateTime = new Date() + const eventId = crypto.randomUUID() const idpExpirationDateTime = new Date() idpExpirationDateTime.setMinutes(idpExpirationDateTime.getMinutes() + 30) it('Should insert a single specified session', async function () { try { - const eventId = crypto.randomUUID() const excessAuthorizationDateTime = new Date() excessAuthorizationDateTime.setMinutes(excessAuthorizationDateTime.getMinutes() + 45) const result = await postSessions.insertSessionBySubIss( @@ -144,10 +144,19 @@ describe('Insert Update and Delete tests', function () { 'eydsagdsadahdhwwgywqrwqrqrwqwqy' ) assert(result, 'Inserted successfully') + + // get session + const session = await postSessions.getSessionByEventId(eventId) + assert.strictEqual(eventId, session.event_id) } catch (error) { assert(false, error.message) } }) + it('Should return null for invalid session event_id', async function () { + const invalidEventId = 'invalid_event_id' + const session = await postSessions.getSessionByEventId(invalidEventId) + assert.strictEqual(null, session) + }) it('Should update a single specified session', async function () { const scope = 'dummy_permission:dummyA' const idpToken = 'eydsagdsadahdhwwgywqrwqrqrwqwqy' @@ -162,6 +171,10 @@ describe('Insert Update and Delete tests', function () { assert(false, error.message) } }) + it('Should get a session by sub claim, user_context, and iss', async function () { + const session = await postSessions.getSessionBySubIss('dummy_subclaim', '', 'https://dummytenant') + assert.strictEqual(eventId, session.event_id) + }) }) }) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index d1c0d6b..6444a01 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -57,6 +57,8 @@ The following variables are required to be configured. |--------------------------|----------------------------------------------------------| | PGURI | Database connection string | | PGSCHEMA | Database schema name | +| KNEX_POOL_MIN | (Optional) Min number of db pool connections, default to 1 | +| KNEX_POOL_MAX | (Optional) Max number of db pool connections, default to 100 | | TOKEN_LIFETIME_SECONDS | Number of seconds Access Token is valid | | SESSION_LIFETIME_SECONDS | Number of seconds Refresh Token is valid | | ISSUER_WHITELIST | Comma separated list of authorized Issuer Servers | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index a5accb5..f75adec 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -22,7 +22,7 @@ The Usher ships with a mock identity provider that simulates Auth0's API endpoin The mock identity provider is fairly simple, and returns tokens with the `iss` (issuer) claim set based on the hostname you use when accessing it. For the purposes of this quickstart, given the identities in the test database, you should ensure the mock identity provider is accessible via the hostname `idp.dmgt.com.mock.localhost`. Depending on your operating system, `docker compose` may create an alias (check this by running `ping idp.dmgt.com.mock.localhost`). However, if it does not, then manually add to `/etc/hosts` an entry like: -``` +```text 127.0.0.1 idp.dmgt.com.mock.localhost ``` diff --git a/mockidentityprovider/src/api_endpoints/endpoint_generate_new_keys.js b/mockidentityprovider/src/api_endpoints/endpoint_generate_new_keys.js index 2c92470..eb535a5 100644 --- a/mockidentityprovider/src/api_endpoints/endpoint_generate_new_keys.js +++ b/mockidentityprovider/src/api_endpoints/endpoint_generate_new_keys.js @@ -1,4 +1,4 @@ -const { generateKeyPairSync } = require('crypto') +const { generateKeyPairSync } = require('node:crypto') var keystore = require('../utils/keystore.js') async function generateAndInsertNewKeys () { diff --git a/server/package-lock.json b/server/package-lock.json index 6cebd6d..9cf6080 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dmgt-tech/the-usher-server", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dmgt-tech/the-usher-server", - "version": "2.1.1", + "version": "2.1.2", "license": "MIT", "dependencies": { "cors": "2.8.5", @@ -29,7 +29,7 @@ "winston-aws-cloudwatch": "3.0.0" }, "devDependencies": { - "mocha": "^10.7.0", + "mocha": "^10.7.3", "node-fetch": "2.7.0", "nodemon": "3.1.4" }, @@ -39,15 +39,15 @@ }, "../database": { "name": "@dmgt-tech/the-usher-server-database", - "version": "2.1.1", + "version": "2.1.2", "license": "MIT", "dependencies": { "dotenv": "16.4.5", "knex": "3.1.0", - "pg": "8.11.3" + "pg": "8.12.0" }, "devDependencies": { - "mocha": "^10.7.0" + "mocha": "^10.7.3" } }, "../database/node_modules/ansi-colors": { @@ -3009,9 +3009,9 @@ } }, "node_modules/mocha": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.0.tgz", - "integrity": "sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", "dev": true, "dependencies": { "ansi-colors": "^4.1.3", diff --git a/server/package.json b/server/package.json index 99f4ea3..2cdf296 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@dmgt-tech/the-usher-server", - "version": "2.1.1", + "version": "2.1.2", "description": "The Usher Authorization Server", "engines": { "node": ">=18" @@ -39,7 +39,7 @@ "winston-aws-cloudwatch": "3.0.0" }, "devDependencies": { - "mocha": "^10.7.0", + "mocha": "^10.7.3", "node-fetch": "2.7.0", "nodemon": "3.1.4" } diff --git a/server/src/api_endpoints/endpoint_generate_new_keys.js b/server/src/api_endpoints/endpoint_generate_new_keys.js index 791b09d..9008623 100644 --- a/server/src/api_endpoints/endpoint_generate_new_keys.js +++ b/server/src/api_endpoints/endpoint_generate_new_keys.js @@ -1,4 +1,4 @@ -const { generateKeyPairSync } = require('crypto') +const { generateKeyPairSync } = require('node:crypto') var keystore = require('database/layer/db-keys') function generateAndInsertNewKeys () { diff --git a/server/src/api_endpoints/endpoint_self_refresh_token.js b/server/src/api_endpoints/endpoint_self_refresh_token.js index 0997ba4..b80744a 100644 --- a/server/src/api_endpoints/endpoint_self_refresh_token.js +++ b/server/src/api_endpoints/endpoint_self_refresh_token.js @@ -18,7 +18,7 @@ async function issueSelfRefreshToken (req, res, next) { // If session is expired, delete it from the sessions table and deny the user a JWT. const sessionLifetimeExpiry = tokenUtils.calculateSessionLifetimeExpiry(session.idp_expirationtime) if (sessionLifetimeExpiry <= 0) { - await dbSessions.deleteSessionByPersonaKey(session.personaKey) + await dbSessions.deleteSessionByPersonaKey(session.personakey) return next(createError(403, 'Forbidden: Refresh token has expired. Unable to issue new JWT access token.')) } diff --git a/server/the-usher.js b/server/the-usher.js index 8dfe72a..422701a 100644 --- a/server/the-usher.js +++ b/server/the-usher.js @@ -1,12 +1,12 @@ const createError = require('http-errors') const usherCors = require('cors') const express = require('express') -const fs = require('fs') +const fs = require('node:fs') const helmet = require('helmet') -const http = require('http') +const http = require('node:http') const jsyaml = require('js-yaml') const oasTools = require('oas-tools') -const path = require('path') +const path = require('node:path') const serverless = require('serverless-http') const env = require('./server-env')